Skip to content
File

Blob: src/workerd/jsg/fast-api-test.c++

13.6 KB
1#include "jsg-test.h"
2 
3#include <workerd/util/autogate.h>
4 
5namespace workerd::jsg::test {
6namespace {
7 
8using jsg::CallCounter;
9 
10struct WrappedInt {
11 int32_t i;
12 
13 JSG_STRUCT(i);
14};
15 
16class StaticMethodContainer: public jsg::Object {
17 public:
18 StaticMethodContainer() = default;
19 
20 static int32_t staticMethod() {
21 return 42;
22 }
23 
24 uint32_t getValue() const {
25 return value;
26 }
27 
28 void setValue(uint32_t value_) {
29 value = value_;
30 }
31 
32 JSG_RESOURCE_TYPE(StaticMethodContainer) {
33 JSG_STATIC_METHOD(staticMethod);
34 JSG_PROTOTYPE_PROPERTY(value, getValue, setValue);
35 }
36 
37 private:
38 uint32_t value = 42;
39};
40 
41class FastMethodContext: public jsg::Object, public jsg::ContextGlobal {
42 public:
43 int32_t add(int32_t a, int32_t b) {
44 return a + b;
45 }
46 
47 int32_t processValue(v8::Local<v8::Value> value) {
48 KJ_ASSERT(value->IsNumber(), "Value must be a number");
49 return value.As<v8::Int32>()->Value();
50 }
51 
52 // When arbitrary type unwrapping is supported, keep this test as it is.
53 int32_t processObject(v8::Local<v8::Object> obj) {
54 auto& js = jsg::Lock::current();
55 auto context = js.v8Context();
56 auto testKey = v8::String::NewFromUtf8(js.v8Isolate, "test").ToLocalChecked();
57 v8::Local<v8::Value> value;
58 if (obj->Get(context, testKey).ToLocal(&value)) {
59 v8::String::Utf8Value type(js.v8Isolate, value->TypeOf(js.v8Isolate));
60 KJ_ASSERT(value->IsInt32(), "Received ", *type);
61 return value.As<v8::Int32>()->Value();
62 }
63 return 0;
64 }
65 
66 void voidMethod(uint32_t a, uint32_t b) {
67 // Do nothing. This is testing void return type.
68 }
69 
70 int32_t throwError(int32_t code) {
71 if (code > 0) {
72 JSG_FAIL_REQUIRE(TypeError, "Test error with code ", code);
73 }
74 return 0;
75 }
76 
77 int32_t addWithLock(jsg::Lock& js, int32_t a, v8::Local<v8::Value> b) {
78 KJ_ASSERT(b->IsNumber(), "Second parameter must be a number");
79 int32_t bValue = b.As<v8::Int32>()->Value();
80 return a + bValue;
81 }
82 
83 int32_t constAdd(int32_t a, int32_t b) {
84 return a + b;
85 }
86 
87 int32_t constAddWithLock(jsg::Lock& js, int32_t a, int32_t b) {
88 return a + b;
89 }
90 
91 int32_t unwrapStruct(jsg::Lock& js, WrappedInt w) {
92 return w.i;
93 }
94 
95 int32_t unwrapUint(jsg::Lock& js, kj::uint u) {
96 return u;
97 }
98 
99 int32_t unwrapString(jsg::Lock& js, kj::String str) {
100 return str.size();
101 }
102 
103 int32_t unwrapBufferSource(jsg::Lock& js, jsg::BufferSource source) {
104 return source.size();
105 }
106 
107 int32_t unwrapMaybe(jsg::Lock& js, kj::Maybe<kj::String> str) {
108 KJ_IF_SOME(s, str) {
109 return s.size();
110 } else {
111 return -1;
112 }
113 }
114 
115 int32_t unwrapOptional(jsg::Lock& js, jsg::Optional<kj::String> str) {
116 KJ_IF_SOME(s, str) {
117 return s.size();
118 } else {
119 return -1;
120 }
121 }
122 
123 int32_t unwrapLenientOptional(jsg::Lock& js, jsg::LenientOptional<kj::String> str) {
124 KJ_IF_SOME(s, str) {
125 return s.size();
126 } else {
127 return -1;
128 }
129 }
130 
131 jsg::Ref<StaticMethodContainer> newContainer(jsg::Lock& js) {
132 return js.alloc<StaticMethodContainer>();
133 }
134 
135 JSG_RESOURCE_TYPE(FastMethodContext) {
136 JSG_NESTED_TYPE(StaticMethodContainer);
137 
138 JSG_METHOD(add);
139 JSG_METHOD(processValue);
140 JSG_METHOD(processObject);
141 JSG_METHOD(voidMethod);
142 JSG_METHOD(throwError);
143 JSG_METHOD(addWithLock);
144 JSG_METHOD(constAdd);
145 JSG_METHOD(constAddWithLock);
146 JSG_METHOD(unwrapStruct);
147 JSG_METHOD(unwrapUint);
148 JSG_METHOD(unwrapString);
149 JSG_METHOD(unwrapBufferSource);
150 JSG_METHOD(unwrapMaybe);
151 JSG_METHOD(unwrapOptional);
152 JSG_METHOD(unwrapLenientOptional);
153 
154 JSG_METHOD(newContainer);
155 }
156};
157 
158JSG_DECLARE_DEBUG_ISOLATE_TYPE(
159 FastMethodIsolate, FastMethodContext, WrappedInt, StaticMethodContainer);
160 
161jsg::V8System v8System({"--allow-natives-syntax"});
162 
163struct Test {
164 kj::LiteralStringConst expr;
165 kj::LiteralStringConst expectedReturnType;
166 kj::LiteralStringConst expectedReturnValue;
167 kj::LiteralStringConst target = ""_kjc;
168 kj::uint expectedSlowCount = 1;
169};
170 
171CallCounter runTest(Test test) {
172 jsg::callCounter.reset();
173 JsgConfig config = {
174 .fastApiEnabled = true,
175 };
176 jsg::test::Evaluator<FastMethodContext, FastMethodIsolate, JsgConfig> e(v8System, config);
177 
178 auto target = test.target == ""_kjc ? kj::str(test.expr) : kj::str(test.target, ".", test.expr);
179 e.expectEval(target, test.expectedReturnType, test.expectedReturnValue);
180 KJ_ASSERT(jsg::callCounter == CallCounter(test.expectedSlowCount, 0));
181 
182 e.expectEval(kj::str("const fastCall = () => { return ", target,
183 "; }; "
184 "%PrepareFunctionForOptimization(fastCall); "
185 "fastCall(); "
186 "%OptimizeFunctionOnNextCall(fastCall); "
187 "fastCall()"),
188 test.expectedReturnType, test.expectedReturnValue);
189 return jsg::callCounter;
190}
191 
192KJ_TEST("v8::Local<v8::Value> and v8::Local<v8::Object> as fast method parameters") {
193 util::Autogate::initAutogateNamesForTest({"v8-fast-api"_kj});
194 KJ_ASSERT(runTest({"processValue(42)"_kjc, "number"_kjc, "42"_kjc}) == CallCounter(2, 1));
195 KJ_ASSERT(
196 runTest({"processObject({test: 123})"_kjc, "number"_kjc, "123"_kjc}) == CallCounter(2, 1));
197}
198 
199KJ_TEST("Lock& as the first parameter in fast method calls") {
200 KJ_ASSERT(runTest({"addWithLock(3, 4)"_kjc, "number"_kjc, "7"_kjc}) == CallCounter(2, 1));
201}
202 
203KJ_TEST("Const methods in fast method calls") {
204 KJ_ASSERT(runTest({"constAdd(3, 4)"_kjc, "number"_kjc, "7"_kjc}) == CallCounter(2, 1));
205}
206 
207KJ_TEST("Const methods with Lock& in fast method calls") {
208 KJ_ASSERT(runTest({"constAddWithLock(3, 4)"_kjc, "number"_kjc, "7"_kjc}) == CallCounter(2, 1));
209}
210 
211KJ_TEST("type unwrapping arguments") {
212 KJ_ASSERT(runTest({"unwrapUint(4)"_kjc, "number"_kjc, "4"_kjc}) == CallCounter(2, 1));
213 KJ_ASSERT(runTest({"unwrapStruct({i: 3})"_kjc, "number"_kjc, "3"_kjc}) == CallCounter(2, 1));
214 KJ_ASSERT(runTest({"unwrapString('0123')"_kjc, "number"_kjc, "4"_kjc}) == CallCounter(2, 1));
215 KJ_ASSERT(runTest({"unwrapBufferSource(new Uint8Array(256))"_kjc, "number"_kjc, "256"_kjc}) ==
216 CallCounter(2, 1));
217 KJ_ASSERT(runTest({"unwrapMaybe(undefined)"_kjc, "number"_kjc, "-1"_kjc}) == CallCounter(2, 1));
218 KJ_ASSERT(runTest({"unwrapMaybe('foo')"_kjc, "number"_kjc, "3"_kjc}) == CallCounter(2, 1));
219 KJ_ASSERT(
220 runTest({"unwrapOptional(undefined)"_kjc, "number"_kjc, "-1"_kjc}) == CallCounter(2, 1));
221 KJ_ASSERT(runTest({"unwrapOptional('foo')"_kjc, "number"_kjc, "3"_kjc}) == CallCounter(2, 1));
222 KJ_ASSERT(runTest({"unwrapLenientOptional(undefined)"_kjc, "number"_kjc, "-1"_kjc}) ==
223 CallCounter(2, 1));
224 KJ_ASSERT(
225 runTest({"unwrapLenientOptional('foo')"_kjc, "number"_kjc, "3"_kjc}) == CallCounter(2, 1));
226 
227 KJ_ASSERT(runTest({"StaticMethodContainer.staticMethod()"_kjc, "number"_kjc, "42"_kjc}) ==
228 CallCounter(2, 1));
229}
230 
231KJ_TEST("Fast methods should work with getters/setters") {
232 KJ_ASSERT(
233 runTest({"value"_kjc, "number"_kjc, "42"_kjc, "newContainer()"_kjc, 2}) == CallCounter(5, 1));
234 KJ_ASSERT(runTest({"value = 12"_kjc, "number"_kjc, "12"_kjc, "newContainer()"_kjc, 2}) ==
235 CallCounter(5, 1));
236}
237 
238KJ_TEST("Fast methods properly catch JSG_FAIL_REQUIRE errors") {
239 jsg::callCounter.reset();
240 JsgConfig config = {
241 .fastApiEnabled = true,
242 };
243 jsg::test::Evaluator<FastMethodContext, FastMethodIsolate, JsgConfig> e(v8System, config);
244 
245 // Test that directly calling the method results in an error
246 e.expectEval("throwError(42)", "throws", "TypeError: Test error with code 42");
247 KJ_ASSERT(jsg::callCounter == CallCounter(1, 0));
248 
249 // First run a non-throwing call to allow optimization
250 e.expectEval("throwError(0)", "number", "0");
251 KJ_ASSERT(jsg::callCounter == CallCounter(2, 0));
252 
253 e.expectEval("function fastThrow(code) { return throwError(code); }; "
254 "%PrepareFunctionForOptimization(fastThrow); "
255 "fastThrow(0); "
256 "%OptimizeFunctionOnNextCall(fastThrow); "
257 "fastThrow(42)",
258 "throws", "TypeError: Test error with code 42");
259 
260 // The counts should now include both the slow path and fast path calls
261 // 3 slow path calls (original direct call + non-throwing call + the initial optimization call that didn't throw)
262 // 1 fast path call (the optimized call that threw an error)
263 KJ_ASSERT(jsg::callCounter == CallCounter(3, 1));
264}
265 
266KJ_TEST("isFastApiCompatible Detection") {
267 static_assert(isFastApiCompatible<void (FastMethodContext::*)(jsg::Lock&, bool)>,
268 "lock is accepted only as first argument");
269 
270 // Method type declarations
271 // -----------------------
272 
273 // Compatible basic types
274 using VoidMethod = void (FastMethodContext::*)();
275 using IntMethod = int32_t (FastMethodContext::*)(int32_t);
276 using BoolMethod = bool (FastMethodContext::*)(double, bool);
277 using FloatMethod = float (FastMethodContext::*)(int32_t, float);
278 
279 // V8 Local types as parameters - should be compatible
280 using V8ValueParamMethod = int32_t (FastMethodContext::*)(v8::Local<v8::Value>);
281 using V8ObjectParamMethod = int32_t (FastMethodContext::*)(v8::Local<v8::Object>);
282 
283 // Const method variants - should be compatible
284 using ConstIntMethod = int32_t (FastMethodContext::*)(int32_t) const;
285 using ConstFloatMethod = float (FastMethodContext::*)(float) const;
286 
287 // Methods with Lock& as first parameter - should be compatible
288 using LockFirstMethod = int32_t (FastMethodContext::*)(jsg::Lock&, int32_t);
289 using LockFirstVoidMethod = void (FastMethodContext::*)(jsg::Lock&, bool);
290 using LockFirstWithV8Local = int32_t (FastMethodContext::*)(jsg::Lock&, v8::Local<v8::Value>);
291 using ConstLockFirstMethod = int32_t (FastMethodContext::*)(jsg::Lock&, int32_t) const;
292 
293 // ---- Non-compatible method types ----
294 
295 // Pointer types - not compatible
296 using PointerParamMethod = void* (FastMethodContext::*)(void*);
297 using PointerReturnMethod = void* (FastMethodContext::*)();
298 
299 // V8 Local as return type - not compatible
300 using V8ReturnMethod = v8::Local<v8::Value> (FastMethodContext::*)(int32_t);
301 
302 // Non-primitive types - not compatible
303 using StringMethod = kj::String (FastMethodContext::*)(kj::String);
304 using ComplexMethod = Ref<FastMethodContext> (FastMethodContext::*)(jsg::Lock&);
305 using KjArrayMethod = kj::Array<int> (FastMethodContext::*)(int32_t);
306 using PromiseMethod = jsg::Promise<int> (FastMethodContext::*)(int32_t);
307 using MaybeVoidMethod = kj::Maybe<void> (FastMethodContext::*)();
308 using StaticMethodContainerMethod = void(StaticMethodContainer);
309 using KjPromiseMethod = void (FastMethodContext::*)(kj::Promise<void>);
310 using JsgPromiseMethod = void (FastMethodContext::*)(jsg::Promise<void>);
311 
312 // Static assertions for compatible method types
313 // --------------------------------------------
314 
315 // Basic primitives
316 static_assert(isFastApiCompatible<VoidMethod>, "Void methods should be fast-method compatible");
317 static_assert(isFastApiCompatible<IntMethod>, "Integer methods should be fast-method compatible");
318 static_assert(
319 isFastApiCompatible<BoolMethod>, "Boolean methods should be fast-method compatible");
320 static_assert(isFastApiCompatible<FloatMethod>, "Float methods should be fast-method compatible");
321 
322 // V8 Local parameters
323 static_assert(isFastApiCompatible<V8ValueParamMethod>,
324 "Methods with v8::Local<v8::Value> parameters should be fast-method compatible");
325 static_assert(isFastApiCompatible<V8ObjectParamMethod>,
326 "Methods with v8::Local<v8::Object> parameters should be fast-method compatible");
327 
328 // Const methods
329 static_assert(
330 isFastApiCompatible<ConstIntMethod>, "Const methods should be fast-method compatible");
331 static_assert(isFastApiCompatible<ConstFloatMethod>,
332 "Const methods with float should be fast-method compatible");
333 
334 // Lock& as first parameter
335 static_assert(isFastApiCompatible<LockFirstMethod>,
336 "Methods with Lock& as first parameter should be fast-method compatible");
337 static_assert(isFastApiCompatible<LockFirstVoidMethod>,
338 "Void methods with Lock& as first parameter should be fast-method compatible");
339 static_assert(isFastApiCompatible<LockFirstWithV8Local>,
340 "Methods with Lock& and v8::Local params should be fast-method compatible");
341 static_assert(isFastApiCompatible<ConstLockFirstMethod>,
342 "Const methods with Lock& as first parameter should be fast-method compatible");
343 
344 // Static assertions for incompatible method types
345 // ----------------------------------------------
346 
347 // Pointer types
348 static_assert(!isFastApiCompatible<PointerParamMethod>,
349 "Methods with pointer parameters should not be fast-method compatible");
350 static_assert(!isFastApiCompatible<PointerReturnMethod>,
351 "Methods returning pointers should not be fast-method compatible");
352 
353 // V8 Local return type
354 static_assert(!isFastApiCompatible<V8ReturnMethod>,
355 "Methods returning v8::Local<v8::Value> should not be fast-method compatible");
356 
357 // Complex types
358 static_assert(!isFastApiCompatible<StringMethod>,
359 "Methods with kj::String parameters should not be fast-method compatible");
360 static_assert(!isFastApiCompatible<ComplexMethod>,
361 "Methods with Ref return types should not be fast-method compatible");
362 static_assert(!isFastApiCompatible<KjArrayMethod>,
363 "Methods returning kj::Array should not be fast-method compatible");
364 static_assert(!isFastApiCompatible<PromiseMethod>,
365 "Methods returning Promise should not be fast-method compatible");
366 static_assert(!isFastApiCompatible<MaybeVoidMethod>,
367 "Methods returning Maybe<void> should not be fast-method compatible");
368 static_assert(isFastApiCompatible<StaticMethodContainerMethod>, "This should be compatible");
369 static_assert(!isFastApiCompatible<KjPromiseMethod>, "kj::Promise is not compatible");
370 static_assert(!isFastApiCompatible<JsgPromiseMethod>, "jsg::Promise is not compatible");
371}
372 
373} // namespace
374} // namespace workerd::jsg::test