| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 4da75c2 commit d1fc925
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -141,6 +141,100 @@ describe("test PostFrameCallback", function () { | |||
| 141 | 141 | }); | |
| 142 | 142 | }); | |
| 143 | 143 | ||
| 144 | + describe("requestAnimationFrame", function () { | ||
| 145 | + const defaultWaitTime = 300; | ||
| 146 | + | ||
| 147 | + it("is exposed under the standard global names", () => { | ||
| 148 | + expect(typeof global.requestAnimationFrame).toBe("function"); | ||
| 149 | + expect(typeof global.cancelAnimationFrame).toBe("function"); | ||
| 150 | + }); | ||
| 151 | + | ||
| 152 | + it("throws on non-function callbacks and ignores bogus cancel handles", () => { | ||
| 153 | + expect(() => global.requestAnimationFrame()).toThrowError(TypeError); | ||
| 154 | + expect(() => global.requestAnimationFrame(null)).toThrowError(TypeError); | ||
| 155 | + expect(() => global.requestAnimationFrame("")).toThrowError(TypeError); | ||
| 156 | + expect(() => global.cancelAnimationFrame()).not.toThrow(); | ||
| 157 | + expect(() => global.cancelAnimationFrame(null)).not.toThrow(); | ||
| 158 | + expect(() => global.cancelAnimationFrame(-1)).not.toThrow(); | ||
| 159 | + expect(() => global.cancelAnimationFrame(Number.MAX_SAFE_INTEGER)).not.toThrow(); | ||
| 160 | + }); | ||
| 161 | + | ||
| 162 | + it("returns a handle and passes a single performance-timeline timestamp", (done) => { | ||
| 163 | + const handle = global.requestAnimationFrame(function (timestamp) { | ||
| 164 | + expect(arguments.length).toBe(1); | ||
| 165 | + expect(typeof timestamp).toBe("number"); | ||
| 166 | + const now = performance.now(); | ||
| 167 | + expect(timestamp).toBeGreaterThan(0); | ||
| 168 | + expect(timestamp).not.toBeGreaterThan(now); | ||
| 169 | + expect(now - timestamp).toBeLessThan(250); | ||
| 170 | + done(); | ||
| 171 | + }); | ||
| 172 | + expect(typeof handle).toBe("number"); | ||
| 173 | + expect(handle).toBeGreaterThan(0); | ||
| 174 | + }); | ||
| 175 | + | ||
| 176 | + it("runs the same function once per request", (done) => { | ||
| 177 | + let callCount = 0; | ||
| 178 | + const callback = () => { | ||
| 179 | + callCount++; | ||
| 180 | + }; | ||
| 181 | + const first = global.requestAnimationFrame(callback); | ||
| 182 | + const second = global.requestAnimationFrame(callback); | ||
| 183 | + expect(second).not.toBe(first); | ||
| 184 | + setTimeout(() => { | ||
| 185 | + expect(callCount).toBe(2); | ||
| 186 | + done(); | ||
| 187 | + }, defaultWaitTime); | ||
| 188 | + }); | ||
| 189 | + | ||
| 190 | + it("cancels only the cancelled request", (done) => { | ||
| 191 | + let cancelledRan = false; | ||
| 192 | + let keptRan = false; | ||
| 193 | + const cancelled = global.requestAnimationFrame(() => { | ||
| 194 | + cancelledRan = true; | ||
| 195 | + }); | ||
| 196 | + global.requestAnimationFrame(() => { | ||
| 197 | + keptRan = true; | ||
| 198 | + }); | ||
| 199 | + global.cancelAnimationFrame(cancelled); | ||
| 200 | + setTimeout(() => { | ||
| 201 | + expect(cancelledRan).toBe(false); | ||
| 202 | + expect(keptRan).toBe(true); | ||
| 203 | + done(); | ||
| 204 | + }, defaultWaitTime); | ||
| 205 | + }); | ||
| 206 | + | ||
| 207 | + it("chains frames when the callback re-requests itself", (done) => { | ||
| 208 | + const timestamps = []; | ||
| 209 | + const callback = (timestamp) => { | ||
| 210 | + timestamps.push(timestamp); | ||
| 211 | + if (timestamps.length === 1) { | ||
| 212 | + global.requestAnimationFrame(callback); | ||
| 213 | + } | ||
| 214 | + }; | ||
| 215 | + global.requestAnimationFrame(callback); | ||
| 216 | + setTimeout(() => { | ||
| 217 | + expect(timestamps.length).toBe(2); | ||
| 218 | + expect(timestamps[1]).not.toBeLessThan(timestamps[0]); | ||
| 219 | + done(); | ||
| 220 | + }, defaultWaitTime); | ||
| 221 | + }); | ||
| 222 | + | ||
| 223 | + it("does not disturb __postFrameCallback dedupe for the same function", (done) => { | ||
| 224 | + let callCount = 0; | ||
| 225 | + const callback = () => { | ||
| 226 | + callCount++; | ||
| 227 | + }; | ||
| 228 | + global.__postFrameCallback(callback); | ||
| 229 | + global.requestAnimationFrame(callback); | ||
| 230 | + global.__postFrameCallback(callback); | ||
| 231 | + setTimeout(() => { | ||
| 232 | + expect(callCount).toBe(2); | ||
| 233 | + done(); | ||
| 234 | + }, defaultWaitTime); | ||
| 235 | + }); | ||
| 236 | + }); | ||
| 237 | + | ||
| 144 | 238 | // The two implementations behind __postFrameCallback (NDK AChoreographer, | |
| 145 | 239 | // android.view.Choreographer for API < 24) must be indistinguishable from JS. | |
| 146 | 240 | // A modern device always selects the NDK one, so the Java bridge is only | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -151,8 +151,12 @@ void ResolveFrameCallbacksClass(JEnv& env) { | |||
| 151 | 151 | using EntryId = uintptr_t; | |
| 152 | 152 | ||
| 153 | 153 | struct FrameCallbackEntry { | |
| 154 | - FrameCallbackEntry(Isolate* isolate, Local<Function> callback, EntryId id) | ||
| 155 | - : isolate_(isolate), callback_(isolate, callback), id_(id) { | ||
| 154 | + FrameCallbackEntry(Isolate* isolate, Local<Function> callback, EntryId id, | ||
| 155 | + bool isAnimationFrame = false) | ||
| 156 | + : isolate_(isolate), | ||
| 157 | + callback_(isolate, callback), | ||
| 158 | + id_(id), | ||
| 159 | + isAnimationFrame_(isAnimationFrame) { | ||
| 156 | 160 | } | |
| 157 | 161 | ||
| 158 | 162 | ~FrameCallbackEntry() { | |
@@ -200,6 +204,13 @@ struct FrameCallbackEntry { | |||
| 200 | 204 | Isolate* isolate_; | |
| 201 | 205 | Global<Function> callback_; | |
| 202 | 206 | EntryId id_; | |
| 207 | + /* | ||
| 208 | + * requestAnimationFrame entries are anonymous one-shots addressed only by | ||
| 209 | + * their returned handle: they never dedupe by function, take the single | ||
| 210 | + * spec-mandated timestamp argument, and cancelAnimationFrame may only | ||
| 211 | + * touch entries carrying this flag. | ||
| 212 | + */ | ||
| 213 | + bool isAnimationFrame_; | ||
| 203 | 214 | jobject javaCallback_ = nullptr; | |
| 204 | 215 | ||
| 205 | 216 | private: | |
@@ -273,15 +284,23 @@ void Dispatch(EntryId id, int64_t frameTimeNanos) { | |||
| 273 | 284 | ||
| 274 | 285 | entry->MarkUnscheduled(); | |
| 275 | 286 | ||
| 287 | + Local<Value> timelineMillis = | ||
| 288 | + Number::New(isolate, Performance::MonotonicNanosToTimelineMillis( | ||
| 289 | + isolate, frameTimeNanos)); | ||
| 276 | 290 | Local<Value> args[2] = { | |
| 277 | 291 | Number::New(isolate, (double) frameTimeNanos), | |
| 278 | - Number::New(isolate, Performance::MonotonicNanosToTimelineMillis( | ||
| 279 | - isolate, frameTimeNanos)), | ||
| 292 | + timelineMillis, | ||
| 280 | 293 | }; | |
| 281 | 294 | ||
| 282 | 295 | TryCatch tc(isolate); | |
| 283 | 296 | ||
| 284 | - cb->Call(context, context->Global(), 2, args); // ignore JS return value | ||
| 297 | + if (entry->isAnimationFrame_) { | ||
| 298 | + // spec signature: a single DOMHighResTimeStamp on this isolate's | ||
| 299 | + // performance timeline | ||
| 300 | + cb->Call(context, context->Global(), 1, &timelineMillis); | ||
| 301 | + } else { | ||
| 302 | + cb->Call(context, context->Global(), 2, args); // ignore JS return value | ||
| 303 | + } | ||
| 285 | 304 | ||
| 286 | 305 | // Re-resolve: the callback may have rescheduled or removed itself. | |
| 287 | 306 | entry = FindEntryById(id); | |
@@ -436,6 +455,58 @@ void FrameCallbacks::RemoveFrameCallback(const FunctionCallbackInfo<Value>& args | |||
| 436 | 455 | } | |
| 437 | 456 | } | |
| 438 | 457 | ||
| 458 | + void FrameCallbacks::RequestAnimationFrame(const FunctionCallbackInfo<Value>& args) { | ||
| 459 | + Isolate* isolate = args.GetIsolate(); | ||
| 460 | + Locker locker(isolate); | ||
| 461 | + Isolate::Scope isolateScope(isolate); | ||
| 462 | + HandleScope handleScope(isolate); | ||
| 463 | + Local<Context> context = isolate->GetCurrentContext(); | ||
| 464 | + Context::Scope contextScope(context); | ||
| 465 | + | ||
| 466 | + if (args.Length() < 1 || !args[0]->IsFunction()) { | ||
| 467 | + isolate->ThrowException(Exception::TypeError(String::NewFromUtf8Literal( | ||
| 468 | + isolate, "Animation frame callback argument is not a function"))); | ||
| 469 | + return; | ||
| 470 | + } | ||
| 471 | + | ||
| 472 | + // Every call registers its own entry: the spec runs a function as many | ||
| 473 | + // times as it was requested, so no dedupe by function here. | ||
| 474 | + EntryId id = ++entryCount_; | ||
| 475 | + FrameCallbackEntry* entry; | ||
| 476 | + { | ||
| 477 | + std::lock_guard<std::mutex> lock(entriesMutex_); | ||
| 478 | + auto inserted = entries_.emplace( | ||
| 479 | + id, std::make_unique<FrameCallbackEntry>( | ||
| 480 | + isolate, args[0].As<Function>(), id, | ||
| 481 | + /* isAnimationFrame */ true)); | ||
| 482 | + NS_DCHECK(inserted.second && "Frame callback ID should not be duplicated"); | ||
| 483 | + entry = inserted.first->second.get(); | ||
| 484 | + } | ||
| 485 | + | ||
| 486 | + entry->MarkScheduled(); | ||
| 487 | + Post(entry, 0); | ||
| 488 | + args.GetReturnValue().Set(Number::New(isolate, (double) id)); | ||
| 489 | + } | ||
| 490 | + | ||
| 491 | + void FrameCallbacks::CancelAnimationFrame(const FunctionCallbackInfo<Value>& args) { | ||
| 492 | + Isolate* isolate = args.GetIsolate(); | ||
| 493 | + Locker locker(isolate); | ||
| 494 | + Isolate::Scope isolateScope(isolate); | ||
| 495 | + HandleScope handleScope(isolate); | ||
| 496 | + Local<Context> context = isolate->GetCurrentContext(); | ||
| 497 | + Context::Scope contextScope(context); | ||
| 498 | + | ||
| 499 | + // per spec an unknown or malformed handle is a silent no-op | ||
| 500 | + if (args.Length() < 1 || !args[0]->IsNumber()) { | ||
| 501 | + return; | ||
| 502 | + } | ||
| 503 | + EntryId id = (EntryId) args[0]->IntegerValue(context).FromMaybe(0); | ||
| 504 | + FrameCallbackEntry* entry = FindEntryById(id); | ||
| 505 | + if (entry != nullptr && entry->isAnimationFrame_) { | ||
| 506 | + entry->MarkRemoved(); | ||
| 507 | + } | ||
| 508 | + } | ||
| 509 | + | ||
| 439 | 510 | void FrameCallbacks::RemoveIsolateEntries(Isolate* isolate) { | |
| 440 | 511 | // Detached first, destroyed after the mutex is released: the destructors | |
| 441 | 512 | // call into Java. | |
@@ -454,6 +525,15 @@ void FrameCallbacks::RemoveIsolateEntries(Isolate* isolate) { | |||
| 454 | 525 | } | |
| 455 | 526 | ||
| 456 | 527 | void FrameCallbacks::Init(Isolate* isolate, Local<ObjectTemplate> globalTemplate) { | |
| 528 | + globalTemplate->Set( | ||
| 529 | + ArgConverter::ConvertToV8String(isolate, "requestAnimationFrame"), | ||
| 530 | + FunctionTemplate::New(isolate, RequestAnimationFrame)); | ||
| 531 | + globalTemplate->Set( | ||
| 532 | + ArgConverter::ConvertToV8String(isolate, "cancelAnimationFrame"), | ||
| 533 | + FunctionTemplate::New(isolate, CancelAnimationFrame)); | ||
| 534 | + // kept for backwards compatibility with callers that predate | ||
| 535 | + // requestAnimationFrame; unlike it, these dedupe by function, take an | ||
| 536 | + // optional delay, and hand the callback the raw frame time as well | ||
| 457 | 537 | globalTemplate->Set( | |
| 458 | 538 | ArgConverter::ConvertToV8String(isolate, "__postFrameCallback"), | |
| 459 | 539 | FunctionTemplate::New(isolate, PostFrameCallback)); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,8 +10,15 @@ | |||
| 10 | 10 | namespace tns { | |
| 11 | 11 | ||
| 12 | 12 | /* | |
| 13 | - * __postFrameCallback(fn[, delayMillis]) / __removeFrameCallback(fn): schedule | ||
| 14 | - * a JS function for the next display frame. | ||
| 13 | + * requestAnimationFrame(fn) / cancelAnimationFrame(handle): the standard | ||
| 14 | + * animation-frame surface. Each request registers its own one-shot entry and | ||
| 15 | + * returns a handle; fn receives a single DOMHighResTimeStamp on the isolate's | ||
| 16 | + * performance timeline. | ||
| 17 | + * | ||
| 18 | + * __postFrameCallback(fn[, delayMillis]) / __removeFrameCallback(fn): the | ||
| 19 | + * compatibility surface predating requestAnimationFrame. Schedules a JS | ||
| 20 | + * function for the next display frame, deduping by function: rescheduling a | ||
| 21 | + * callback that is already pending is a no-op. | ||
| 15 | 22 | * | |
| 16 | 23 | * fn receives two arguments: | |
| 17 | 24 | * fn(frameTimeNanos, performanceMillis) | |
@@ -25,9 +32,8 @@ namespace tns { | |||
| 25 | 32 | * - android.view.Choreographer (com.tns.FrameCallbacks), for API 21-23, | |
| 26 | 33 | * where the NDK API does not exist. | |
| 27 | 34 | * Scheduling is per calling thread, so a worker schedules against its own | |
| 28 | - * looper. Rescheduling a callback that is already pending is a no-op, and | ||
| 29 | - * removal only marks the entry: a posted frame callback cannot be recalled, so | ||
| 30 | - * the dispatch drops it instead. | ||
| 35 | + * looper. Cancellation on either surface only marks the entry: a posted frame | ||
| 36 | + * callback cannot be recalled, so the dispatch drops it instead. | ||
| 31 | 37 | */ | |
| 32 | 38 | class FrameCallbacks { | |
| 33 | 39 | public: | |
@@ -42,6 +48,8 @@ class FrameCallbacks { | |||
| 42 | 48 | static void RemoveIsolateEntries(v8::Isolate* isolate); | |
| 43 | 49 | ||
| 44 | 50 | private: | |
| 51 | + static void RequestAnimationFrame(const v8::FunctionCallbackInfo<v8::Value>& args); | ||
| 52 | + static void CancelAnimationFrame(const v8::FunctionCallbackInfo<v8::Value>& args); | ||
| 45 | 53 | static void PostFrameCallback(const v8::FunctionCallbackInfo<v8::Value>& args); | |
| 46 | 54 | static void RemoveFrameCallback(const v8::FunctionCallbackInfo<v8::Value>& args); | |
| 47 | 55 | }; | |
| Back | FazBrowse Home | New Git URL |
0 commit comments