Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 29 additions & 46 deletions bindings/otel-thread-ctx.cc
Original file line number Diff line number Diff line change
Expand Up @@ -901,62 +901,45 @@ void GetStoredAlsHash(const FunctionCallbackInfo<Value>& args) {
Integer::New(isolate, otel_thread_ctx_nodejs_v1.als_identity_hash));
}

// V8 layout constants captured at addon-compile time from the same V8
// headers Node bundles. Published via the discovery contract so an
// out-of-process reader can decode V8's JSObject / internal hashmap
// layout without doing its own V8-internal-symbol lookups for the
// pointer-compression / sandbox state. Note that nothing published here
// describes our own wrapper: internal field 0 points straight at the
// record, so the reader needs no offset of ours to reach it.
// The nodejs_v1 discovery schema does not publish V8's object layout; it
// fixes it, presuming the V8 Node.js builds by default: 64-bit, pointer
// compression off, sandbox off. These assertions check that presumption
// against the V8 headers we are compiled with, so a build not matching
// the schema will fail to compile.
//
// Each value the reader needs equals one of V8's public constants:
// tagged size (8) kApiTaggedSize
// JSMap table offset (0x18) kJSObjectHeaderSize, because JSCollection
// adds a single `table` field to JSObject
// (deps/v8/src/objects/js-collection.h)
// OrderedHashMap header kFixedArrayHeaderSize, because
// size (0x10) OrderedHashTable derives from FixedArray
// (deps/v8/src/objects/ordered-hash-table.h)
// record slot offset (0x18) kJSObjectHeaderSize plus
// kEmbedderDataSlotExternalPointerOffset,
// which is 0 without the sandbox: internal
// field 0 then holds the raw record pointer
static_assert(v8::internal::kApiTaggedSize == 8,
"nodejs_v1 assumes a V8 built without pointer compression");
static_assert(v8::internal::Internals::kJSObjectHeaderSize == 0x18,
"unexpected V8 JSObject header size");
static_assert(v8::internal::Internals::kFixedArrayHeaderSize == 0x10,
"unexpected V8 FixedArray header size");
#if NODE_MAJOR_VERSION >= 22
constexpr int JS_OBJECT_RECORD_OFFSET =
v8::internal::Internals::kJSObjectHeaderSize +
// Node < 22 lacks this constant; the contract is unusable there anyway,
// as it has no ContinuationPreservedEmbedderData either (see StoreAls).
constexpr int kEmbedderDataSlotExternalPtrOffset =
v8::internal::Internals::kEmbedderDataSlotExternalPointerOffset;
#else
// Node < 22 lacks kEmbedderDataSlotExternalPointerOffset. The discovery
// contract isn't usable on these versions (no ContinuationPreservedEmbedderData
// either — see StoreAls), so this value is published only to keep the
// addon's exported surface consistent across Node majors. A would-be
// reader cannot reach a live record through it.
constexpr int JS_OBJECT_RECORD_OFFSET = 0;
static_assert(kEmbedderDataSlotExternalPtrOffset == 0,
"nodejs_v1 assumes a V8 built without the sandbox");
#endif
constexpr int TAGGED_SIZE = v8::internal::kApiTaggedSize;

// V8 JSMap layout: kTableOffset within the JSMap object holds a tagged
// pointer to the backing OrderedHashMap table. Not exposed in V8's
// public headers; kept in sync with
// deps/v8/src/objects/js-collection.h (JSCollection::kTableOffset)
// and the torque-generated JSCollection layout.
constexpr int JS_MAP_TABLE_OFFSET = 0x18;

// V8 OrderedHashMap layout: the on-heap table starts with a 16-byte
// header before the element_count / deleted_element_count /
// number_of_buckets fields. Not exposed in V8's public headers; kept in
// sync with deps/v8/src/objects/ordered-hash-table.h
// (OrderedHashTable base layout).
constexpr int ORDERED_HASH_MAP_HEADER_SIZE = 0x10;

} // namespace

void OtelThreadCtx::Init(Local<Object> exports) {
CtxWrap::Init(exports);
NODE_SET_METHOD(exports, "otelThreadCtxStoreAls", StoreAls);
NODE_SET_METHOD(exports, "otelThreadCtxGetStoredAlsHash", GetStoredAlsHash);

Isolate* isolate = Isolate::GetCurrent();
Local<Context> ctx = isolate->GetCurrentContext();
auto publish_int = [&](const char* name, int value) {
exports
->Set(ctx,
String::NewFromUtf8(isolate, name).ToLocalChecked(),
Integer::New(isolate, value))
.FromJust();
};
publish_int("otelThreadCtxJsMapTableOffset", JS_MAP_TABLE_OFFSET);
publish_int("otelThreadCtxOrderedHashMapHeaderSize",
ORDERED_HASH_MAP_HEADER_SIZE);
publish_int("otelThreadCtxTaggedSize", TAGGED_SIZE);
publish_int("otelThreadCtxJsObjectRecordOffset", JS_OBJECT_RECORD_OFFSET);
}

} // namespace dd
26 changes: 0 additions & 26 deletions ts/src/otel-thread-ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ import {
export interface ProcessContextAttributes {
readonly 'threadlocal.schema_version': 'nodejs_v1_dev';
readonly 'threadlocal.attribute_key_map': readonly string[];
readonly 'threadlocal.js_object_record_offset': number;
readonly 'threadlocal.tagged_size': number;
readonly 'threadlocal.js_map_table_offset': number;
readonly 'threadlocal.ordered_hash_map_header_size': number;
}

/**
Expand Down Expand Up @@ -142,24 +138,10 @@ interface Addon {
threadContext: ThreadContextCtor;
otelThreadCtxStoreAls(als: AsyncLocalStorage<ThreadContext>): void;
otelThreadCtxGetStoredAlsHash(): number;
otelThreadCtxJsObjectRecordOffset: number;
otelThreadCtxTaggedSize: number;
otelThreadCtxJsMapTableOffset: number;
otelThreadCtxOrderedHashMapHeaderSize: number;
}

const SCHEMA_VERSION = 'nodejs_v1_dev';

// V8 layout constants the addon captured from the V8 headers Node bundles.
// On non-Linux these fall back to values matching Node's standard build
// (no V8 pointer compression, no sandbox); the reader is Linux-only per
// the OTEP anyway, so the fallbacks just keep processContextAttributes
// consistent in shape.
let JS_OBJECT_RECORD_OFFSET = 0x18;
let TAGGED_SIZE = 8;
let JS_MAP_TABLE_OFFSET = 0x18;
let ORDERED_HASH_MAP_HEADER_SIZE = 0x10;

/** {@inheritDoc ThreadContextCtor} */
export let ThreadContext: ThreadContextCtor;

Expand All @@ -183,10 +165,6 @@ if (process.platform === 'linux') {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const findBinding = require('node-gyp-build');
const addon: Addon = findBinding(join(__dirname, '..', '..'));
JS_OBJECT_RECORD_OFFSET = addon.otelThreadCtxJsObjectRecordOffset;
TAGGED_SIZE = addon.otelThreadCtxTaggedSize;
JS_MAP_TABLE_OFFSET = addon.otelThreadCtxJsMapTableOffset;
ORDERED_HASH_MAP_HEADER_SIZE = addon.otelThreadCtxOrderedHashMapHeaderSize;

ThreadContext = addon.threadContext;

Expand Down Expand Up @@ -298,9 +276,5 @@ export function getProcessContextAttributes(
return Object.freeze({
'threadlocal.schema_version': SCHEMA_VERSION,
'threadlocal.attribute_key_map': Object.freeze(keys.slice()),
'threadlocal.js_object_record_offset': JS_OBJECT_RECORD_OFFSET,
'threadlocal.tagged_size': TAGGED_SIZE,
'threadlocal.js_map_table_offset': JS_MAP_TABLE_OFFSET,
'threadlocal.ordered_hash_map_header_size': ORDERED_HASH_MAP_HEADER_SIZE,
}) as ProcessContextAttributes;
}
11 changes: 0 additions & 11 deletions ts/test/test-otel-thread-ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -940,20 +940,9 @@ function captureBytes(opts: {
const pca = getProcessContextAttributes(keys);
strictAssert.equal(pca['threadlocal.schema_version'], 'nodejs_v1_dev');
strictAssert.deepEqual(pca['threadlocal.attribute_key_map'], keys);
strictAssert.equal(pca['threadlocal.js_object_record_offset'], 0x18);
strictAssert.equal(pca['threadlocal.tagged_size'], 8);
strictAssert.equal(pca['threadlocal.js_map_table_offset'], 0x18);
strictAssert.equal(
pca['threadlocal.ordered_hash_map_header_size'],
0x10,
);
strictAssert.deepEqual(Object.keys(pca).sort(), [
'threadlocal.attribute_key_map',
'threadlocal.js_map_table_offset',
'threadlocal.js_object_record_offset',
'threadlocal.ordered_hash_map_header_size',
'threadlocal.schema_version',
'threadlocal.tagged_size',
]);
});

Expand Down
Loading