File
Blob: src/workerd/jsg/fast-api-test.c++
| 1 | #include "jsg-test.h" |
| 2 | |
| 3 | #include <workerd/util/autogate.h> |
| 4 | |
| 5 | namespace workerd::jsg::test { |
| 6 | namespace { |
| 7 | |
| 8 | using jsg::CallCounter; |
| 9 | |
| 10 | struct WrappedInt { |
| 11 | int32_t i; |
| 12 | |
| 13 | JSG_STRUCT(i); |
| 14 | }; |
| 15 | |
| 16 | class 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 | |
| 41 | class 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 | |
| 158 | JSG_DECLARE_DEBUG_ISOLATE_TYPE( |
| 159 | FastMethodIsolate, FastMethodContext, WrappedInt, StaticMethodContainer); |
| 160 | |
| 161 | jsg::V8System v8System({"--allow-natives-syntax"}); |
| 162 | |
| 163 | struct 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 | |
| 171 | CallCounter 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 | |
| 192 | KJ_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 | |
| 199 | KJ_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 | |
| 203 | KJ_TEST("Const methods in fast method calls") { |
| 204 | KJ_ASSERT(runTest({"constAdd(3, 4)"_kjc, "number"_kjc, "7"_kjc}) == CallCounter(2, 1)); |
| 205 | } |
| 206 | |
| 207 | KJ_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 | |
| 211 | KJ_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 | |
| 231 | KJ_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 | |
| 238 | KJ_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 | |
| 266 | KJ_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 |