Skip to content

Commit 6b692f0

Browse files
committed
src,test: ensure that V8 fast APIs are called
Adds a debug-only macro that can be used to track when a V8 fast API is called. A map of counters is maintained in in thread-local storage and an internal API can be called to get the total count associated with a call id. Specific tests are added and `crypto.timingSafeEqual` as well as internal documentation are updated to show how to use the macro and test fast API calls without running long loops. PR-URL: #54317 Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Tobias Nießen <[email protected]>
1 parent c0ebbe7 commit 6b692f0

File tree

12 files changed

+284
-3
lines changed

12 files changed

+284
-3
lines changed

doc/contributing/adding-v8-fast-api.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ for example, they may not trigger garbage collection.
2929
* To test fast APIs, make sure to run the tests in a loop with a decent
3030
iterations count to trigger relevant optimizations that prefer the fast API
3131
over the slow one.
32+
* In debug mode (`--debug` or `--debug-node` flags), the fast API calls can be
33+
tracked using the `TRACK_V8_FAST_API_CALL("key")` macro. This can be used to
34+
count how many times fast paths are taken during tests. The key is a global
35+
identifier and should be unique across the codebase.
36+
Use `"binding_name.function_name"` or `"binding_name.function_name.suffix"` to
37+
ensure uniqueness.
3238
* The fast callback must be idempotent up to the point where error and fallback
3339
conditions are checked, because otherwise executing the slow callback might
3440
produce visible side effects twice.
@@ -77,6 +83,7 @@ A typical function that communicates between JavaScript and C++ is as follows.
7783
* On the C++ side:
7884

7985
```cpp
86+
#include "node_debug.h"
8087
#include "v8-fast-api-calls.h"
8188

8289
namespace node {
@@ -102,9 +109,11 @@ A typical function that communicates between JavaScript and C++ is as follows.
102109
const int32_t b,
103110
v8::FastApiCallbackOptions& options) {
104111
if (b == 0) {
112+
TRACK_V8_FAST_API_CALL("custom_namespace.divide.error");
105113
options.fallback = true;
106114
return 0;
107115
} else {
116+
TRACK_V8_FAST_API_CALL("custom_namespace.divide.ok");
108117
return a / b;
109118
}
110119
}
@@ -148,3 +157,42 @@ A typical function that communicates between JavaScript and C++ is as follows.
148157
const int32_t b,
149158
v8::FastApiCallbackOptions& options);
150159
```
160+
161+
* In the unit tests:
162+
163+
Since the fast API function uses `TRACK_V8_FAST_API_CALL`, we can ensure that
164+
the fast paths are taken and test them by writing tests that force
165+
V8 optimizations and check the counters.
166+
167+
```js
168+
// Flags: --expose-internals --no-warnings --allow-natives-syntax
169+
'use strict';
170+
const common = require('../common');
171+
172+
const { internalBinding } = require('internal/test/binding');
173+
// We could also require a function that uses the internal binding internally.
174+
const { divide } = internalBinding('custom_namespace');
175+
176+
if (common.isDebug) {
177+
const { getV8FastApiCallCount } = internalBinding('debug');
178+
179+
// The function that will be optimized. It has to be a function written in
180+
// JavaScript. Since `divide` comes from the C++ side, we need to wrap it.
181+
function testFastPath(a, b) {
182+
return divide(a, b);
183+
}
184+
185+
eval('%PrepareFunctionForOptimization(testFastPath)');
186+
// This call will let V8 know about the argument types that the function expects.
187+
assert.strictEqual(testFastPath(6, 3), 2);
188+
189+
eval('%OptimizeFunctionOnNextCall(testFastPath)');
190+
assert.strictEqual(testFastPath(8, 2), 4);
191+
assert.throws(() => testFastPath(1, 0), {
192+
code: 'ERR_INVALID_STATE',
193+
});
194+
195+
assert.strictEqual(getV8FastApiCallCount('custom_namespace.divide.ok'), 1);
196+
assert.strictEqual(getV8FastApiCallCount('custom_namespace.divide.error'), 1);
197+
}
198+
```

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
'src/node_constants.cc',
108108
'src/node_contextify.cc',
109109
'src/node_credentials.cc',
110+
'src/node_debug.cc',
110111
'src/node_dir.cc',
111112
'src/node_dotenv.cc',
112113
'src/node_env_var.cc',
@@ -229,6 +230,7 @@
229230
'src/node_constants.h',
230231
'src/node_context_data.h',
231232
'src/node_contextify.h',
233+
'src/node_debug.h',
232234
'src/node_dir.h',
233235
'src/node_dotenv.h',
234236
'src/node_errors.h',

src/crypto/crypto_timing.cc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
#include "crypto/crypto_timing.h"
22
#include "crypto/crypto_util.h"
33
#include "env-inl.h"
4+
#include "node.h"
5+
#include "node_debug.h"
46
#include "node_errors.h"
57
#include "v8.h"
6-
#include "node.h"
78

89
#include <openssl/crypto.h>
910

@@ -57,10 +58,12 @@ bool FastTimingSafeEqual(Local<Value> receiver,
5758
uint8_t* data_b;
5859
if (a.length() != b.length() || !a.getStorageIfAligned(&data_a) ||
5960
!b.getStorageIfAligned(&data_b)) {
61+
TRACK_V8_FAST_API_CALL("crypto.timingSafeEqual.error");
6062
options.fallback = true;
6163
return false;
6264
}
6365

66+
TRACK_V8_FAST_API_CALL("crypto.timingSafeEqual.ok");
6467
return CRYPTO_memcmp(data_a, data_b, a.length()) == 0;
6568
}
6669

src/node_binding.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
#define NODE_BUILTIN_PROFILER_BINDINGS(V)
2121
#endif
2222

23+
#ifdef DEBUG
24+
#define NODE_BUILTIN_DEBUG_BINDINGS(V) V(debug)
25+
#else
26+
#define NODE_BUILTIN_DEBUG_BINDINGS(V)
27+
#endif
28+
2329
// A list of built-in bindings. In order to do binding registration
2430
// in node::Init(), need to add built-in bindings in the following list.
2531
// Then in binding::RegisterBuiltinBindings(), it calls bindings' registration
@@ -96,6 +102,7 @@
96102
NODE_BUILTIN_OPENSSL_BINDINGS(V) \
97103
NODE_BUILTIN_ICU_BINDINGS(V) \
98104
NODE_BUILTIN_PROFILER_BINDINGS(V) \
105+
NODE_BUILTIN_DEBUG_BINDINGS(V) \
99106
NODE_BUILTIN_QUIC_BINDINGS(V)
100107

101108
// This is used to load built-in bindings. Instead of using

src/node_debug.cc

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#include "node_debug.h"
2+
3+
#ifdef DEBUG
4+
#include "node_binding.h"
5+
6+
#include "env-inl.h"
7+
#include "util.h"
8+
#include "v8-fast-api-calls.h"
9+
#include "v8.h"
10+
11+
#include <string_view>
12+
#include <unordered_map>
13+
#endif // DEBUG
14+
15+
namespace node {
16+
namespace debug {
17+
18+
#ifdef DEBUG
19+
using v8::Context;
20+
using v8::FastApiCallbackOptions;
21+
using v8::FunctionCallbackInfo;
22+
using v8::Local;
23+
using v8::Number;
24+
using v8::Object;
25+
using v8::Value;
26+
27+
thread_local std::unordered_map<std::string_view, int> v8_fast_api_call_counts;
28+
29+
void TrackV8FastApiCall(std::string_view key) {
30+
v8_fast_api_call_counts[key]++;
31+
}
32+
33+
int GetV8FastApiCallCount(std::string_view key) {
34+
return v8_fast_api_call_counts[key];
35+
}
36+
37+
void GetV8FastApiCallCount(const FunctionCallbackInfo<Value>& args) {
38+
Environment* env = Environment::GetCurrent(args);
39+
if (!args[0]->IsString()) {
40+
env->ThrowError("getV8FastApiCallCount must be called with a string");
41+
return;
42+
}
43+
Utf8Value utf8_key(env->isolate(), args[0]);
44+
args.GetReturnValue().Set(GetV8FastApiCallCount(utf8_key.ToString()));
45+
}
46+
47+
void SlowIsEven(const FunctionCallbackInfo<Value>& args) {
48+
Environment* env = Environment::GetCurrent(args);
49+
if (!args[0]->IsNumber()) {
50+
env->ThrowError("isEven must be called with a number");
51+
return;
52+
}
53+
int64_t value = args[0].As<Number>()->Value();
54+
args.GetReturnValue().Set(value % 2 == 0);
55+
}
56+
57+
bool FastIsEven(Local<Value> receiver,
58+
const int64_t value,
59+
// NOLINTNEXTLINE(runtime/references)
60+
FastApiCallbackOptions& options) {
61+
TRACK_V8_FAST_API_CALL("debug.isEven");
62+
return value % 2 == 0;
63+
}
64+
65+
void SlowIsOdd(const FunctionCallbackInfo<Value>& args) {
66+
Environment* env = Environment::GetCurrent(args);
67+
if (!args[0]->IsNumber()) {
68+
env->ThrowError("isOdd must be called with a number");
69+
return;
70+
}
71+
int64_t value = args[0].As<Number>()->Value();
72+
args.GetReturnValue().Set(value % 2 != 0);
73+
}
74+
75+
bool FastIsOdd(Local<Value> receiver,
76+
const int64_t value,
77+
// NOLINTNEXTLINE(runtime/references)
78+
FastApiCallbackOptions& options) {
79+
TRACK_V8_FAST_API_CALL("debug.isOdd");
80+
return value % 2 != 0;
81+
}
82+
83+
static v8::CFunction fast_is_even(v8::CFunction::Make(FastIsEven));
84+
static v8::CFunction fast_is_odd(v8::CFunction::Make(FastIsOdd));
85+
86+
void Initialize(Local<Object> target,
87+
Local<Value> unused,
88+
Local<Context> context,
89+
void* priv) {
90+
SetMethod(context, target, "getV8FastApiCallCount", GetV8FastApiCallCount);
91+
SetFastMethod(context, target, "isEven", SlowIsEven, &fast_is_even);
92+
SetFastMethod(context, target, "isOdd", SlowIsOdd, &fast_is_odd);
93+
}
94+
#endif // DEBUG
95+
96+
} // namespace debug
97+
} // namespace node
98+
99+
#ifdef DEBUG
100+
NODE_BINDING_CONTEXT_AWARE_INTERNAL(debug, node::debug::Initialize)
101+
#endif // DEBUG

src/node_debug.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma once
2+
3+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
4+
5+
#ifdef DEBUG
6+
#include <string_view>
7+
#endif // DEBUG
8+
9+
namespace node {
10+
namespace debug {
11+
12+
#ifdef DEBUG
13+
void TrackV8FastApiCall(std::string_view key);
14+
int GetV8FastApiCallCount(std::string_view key);
15+
16+
#define TRACK_V8_FAST_API_CALL(key) node::debug::TrackV8FastApiCall(key)
17+
#else // !DEBUG
18+
#define TRACK_V8_FAST_API_CALL(key)
19+
#endif // DEBUG
20+
21+
} // namespace debug
22+
} // namespace node
23+
24+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

test/benchmark/test-benchmark-napi.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ if (!common.isMainThread) {
1010
common.skip('addons are not supported in workers');
1111
}
1212

13-
if (process.features.debug) {
13+
if (common.isDebug) {
1414
common.skip('benchmark does not work with debug build yet');
1515
}
1616
const runBenchmark = require('../common/benchmark');

test/common/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ const isOpenBSD = process.platform === 'openbsd';
143143
const isLinux = process.platform === 'linux';
144144
const isMacOS = process.platform === 'darwin';
145145
const isASan = process.config.variables.asan === 1;
146+
const isDebug = process.features.debug;
146147
const isPi = (() => {
147148
try {
148149
// Normal Raspberry Pi detection is to find the `Raspberry Pi` string in
@@ -280,7 +281,7 @@ function platformTimeout(ms) {
280281
const multipliers = typeof ms === 'bigint' ?
281282
{ two: 2n, four: 4n, seven: 7n } : { two: 2, four: 4, seven: 7 };
282283

283-
if (process.features.debug)
284+
if (isDebug)
284285
ms = multipliers.two * ms;
285286

286287
if (exports.isAIX || exports.isIBMi)
@@ -998,6 +999,7 @@ const common = {
998999
invalidArgTypeHelper,
9991000
isAlive,
10001001
isASan,
1002+
isDebug,
10011003
isDumbTerminal,
10021004
isFreeBSD,
10031005
isLinux,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Flags: --expose-internals --no-warnings --allow-natives-syntax
2+
'use strict';
3+
const common = require('../common');
4+
5+
const assert = require('assert');
6+
const { internalBinding } = require('internal/test/binding');
7+
8+
if (!common.isDebug) {
9+
assert.throws(() => internalBinding('debug'), {
10+
message: 'No such binding: debug'
11+
});
12+
return;
13+
}
14+
15+
const {
16+
getV8FastApiCallCount,
17+
isEven,
18+
isOdd,
19+
} = internalBinding('debug');
20+
21+
assert.throws(() => getV8FastApiCallCount(), {
22+
message: 'getV8FastApiCallCount must be called with a string',
23+
});
24+
25+
function testIsEven() {
26+
for (let i = 0; i < 10; i++) {
27+
assert.strictEqual(isEven(i), i % 2 === 0);
28+
}
29+
}
30+
31+
function testIsOdd() {
32+
for (let i = 0; i < 20; i++) {
33+
assert.strictEqual(isOdd(i), i % 2 !== 0);
34+
}
35+
}
36+
37+
// Should return 0 by default for any string.
38+
assert.strictEqual(getV8FastApiCallCount(''), 0);
39+
assert.strictEqual(getV8FastApiCallCount('foo'), 0);
40+
assert.strictEqual(getV8FastApiCallCount('debug.isEven'), 0);
41+
assert.strictEqual(getV8FastApiCallCount('debug.isOdd'), 0);
42+
43+
eval('%PrepareFunctionForOptimization(testIsEven)');
44+
testIsEven();
45+
eval('%PrepareFunctionForOptimization(testIsOdd)');
46+
testIsOdd();
47+
48+
// Functions should not be optimized yet.
49+
assert.strictEqual(getV8FastApiCallCount('debug.isEven'), 0);
50+
assert.strictEqual(getV8FastApiCallCount('debug.isOdd'), 0);
51+
52+
eval('%OptimizeFunctionOnNextCall(testIsEven)');
53+
testIsEven();
54+
eval('%OptimizeFunctionOnNextCall(testIsOdd)');
55+
testIsOdd();
56+
57+
// Functions should have been optimized and fast path taken.
58+
assert.strictEqual(getV8FastApiCallCount('debug.isEven'), 10);
59+
assert.strictEqual(getV8FastApiCallCount('debug.isOdd'), 20);

test/sequential/test-crypto-timing-safe-equal.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --expose-internals --no-warnings --allow-natives-syntax
12
'use strict';
23
const common = require('../common');
34
if (!common.hasCrypto)
@@ -91,3 +92,25 @@ assert.throws(
9192
name: 'TypeError',
9293
}
9394
);
95+
96+
if (common.isDebug) {
97+
const { internalBinding } = require('internal/test/binding');
98+
const { getV8FastApiCallCount } = internalBinding('debug');
99+
100+
const foo = Buffer.from('foo');
101+
const bar = Buffer.from('bar');
102+
const longer = Buffer.from('longer');
103+
function testFastPath(buf1, buf2) {
104+
return crypto.timingSafeEqual(buf1, buf2);
105+
}
106+
eval('%PrepareFunctionForOptimization(testFastPath)');
107+
assert.strictEqual(testFastPath(foo, bar), false);
108+
eval('%OptimizeFunctionOnNextCall(testFastPath)');
109+
assert.strictEqual(testFastPath(foo, bar), false);
110+
assert.strictEqual(testFastPath(foo, foo), true);
111+
assert.throws(() => testFastPath(foo, longer), {
112+
code: 'ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
113+
});
114+
assert.strictEqual(getV8FastApiCallCount('crypto.timingSafeEqual.ok'), 2);
115+
assert.strictEqual(getV8FastApiCallCount('crypto.timingSafeEqual.error'), 1);
116+
}

0 commit comments

Comments
 (0)