fix(zone.js): setTimeout patch should clean tasksByHandleId cache. (#40586)
Close #40387 Currently zone.js patches `setTimeout` and keeps a `tasksByHandleId` map to keep `timerId` <-> `ZoneTask` relationship. This is needed so that when `clearTimeout(timerId)` is called, zone.js can find the associated `ZoneTask`. Now zone.js set the `tasksByHandleId` map in the `scheduleTask` function, but if the `setTimeout` is running in the `FakeAsyncZoneSpec` or any other `ZoneSpec` with `onScheduleTask` hooks. The `scheduleTask` in `timer` patch may not be invoked. For example: ``` fakeAsync(() => { setTimeout(() => {}); tick(); }); ``` In this case, the `timerId` kept in the `tasksByHandleId` map is not cleared. This is because the `FakeAsyncZoneSpec` in the `onScheduleTask` hook looks like this. ``` onScheduleTask(delegate, ..., task) { fakeAsyncScheduler.setTimeout(task); return task; } ``` Because `FakeAsyncZoneSpec` handles the task itself and it doesn't call `parentDelegate.onScheduleTask`, therefore the default `scheduleTask` in the `timer` patch is not invoked. In this commit, the cleanup logic is moved from `scheduleTask` to `setTimeout` patch entry to avoid the memory leak. PR Close #40586
This commit is contained in:
parent
d9e4d751f0
commit
0652b29f62
@ -29,27 +29,9 @@ export function patchTimer(window: any, setName: string, cancelName: string, nam
|
|||||||
|
|
||||||
function scheduleTask(task: Task) {
|
function scheduleTask(task: Task) {
|
||||||
const data = <TimerOptions>task.data;
|
const data = <TimerOptions>task.data;
|
||||||
function timer(this: unknown) {
|
data.args[0] = function() {
|
||||||
try {
|
return task.invoke.apply(this, arguments);
|
||||||
task.invoke.apply(this, arguments);
|
};
|
||||||
} finally {
|
|
||||||
// issue-934, task will be cancelled
|
|
||||||
// even it is a periodic task such as
|
|
||||||
// setInterval
|
|
||||||
if (!(task.data && task.data.isPeriodic)) {
|
|
||||||
if (typeof data.handleId === 'number') {
|
|
||||||
// in non-nodejs env, we remove timerId
|
|
||||||
// from local cache
|
|
||||||
delete tasksByHandleId[data.handleId];
|
|
||||||
} else if (data.handleId) {
|
|
||||||
// Node returns complex objects as handleIds
|
|
||||||
// we remove task reference from timer object
|
|
||||||
(data.handleId as any)[taskSymbol] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.args[0] = timer;
|
|
||||||
data.handleId = setNative!.apply(window, data.args);
|
data.handleId = setNative!.apply(window, data.args);
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
@ -67,6 +49,32 @@ export function patchTimer(window: any, setName: string, cancelName: string, nam
|
|||||||
undefined,
|
undefined,
|
||||||
args: args
|
args: args
|
||||||
};
|
};
|
||||||
|
const callback = args[0];
|
||||||
|
args[0] = function timer(this: unknown) {
|
||||||
|
try {
|
||||||
|
return callback.apply(this, arguments);
|
||||||
|
} finally {
|
||||||
|
// issue-934, task will be cancelled
|
||||||
|
// even it is a periodic task such as
|
||||||
|
// setInterval
|
||||||
|
|
||||||
|
// https://github.com/angular/angular/issues/40387
|
||||||
|
// Cleanup tasksByHandleId should be handled before scheduleTask
|
||||||
|
// Since some zoneSpec may intercept and doesn't trigger
|
||||||
|
// scheduleFn(scheduleTask) provided here.
|
||||||
|
if (!(options.isPeriodic)) {
|
||||||
|
if (typeof options.handleId === 'number') {
|
||||||
|
// in non-nodejs env, we remove timerId
|
||||||
|
// from local cache
|
||||||
|
delete tasksByHandleId[options.handleId];
|
||||||
|
} else if (options.handleId) {
|
||||||
|
// Node returns complex objects as handleIds
|
||||||
|
// we remove task reference from timer object
|
||||||
|
(options.handleId as any)[taskSymbol] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
const task =
|
const task =
|
||||||
scheduleMacroTaskWithCurrentZone(setName, args[0], options, scheduleTask, clearTask);
|
scheduleMacroTaskWithCurrentZone(setName, args[0], options, scheduleTask, clearTask);
|
||||||
if (!task) {
|
if (!task) {
|
||||||
|
@ -289,6 +289,40 @@ describe('FakeAsyncTestZoneSpec', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should clear internal timerId cache', () => {
|
||||||
|
let taskSpy: jasmine.Spy = jasmine.createSpy('taskGetState');
|
||||||
|
fakeAsyncTestZone
|
||||||
|
.fork({
|
||||||
|
name: 'scheduleZone',
|
||||||
|
onScheduleTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task) => {
|
||||||
|
(task as any)._state = task.state;
|
||||||
|
Object.defineProperty(task, 'state', {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
get: () => {
|
||||||
|
taskSpy();
|
||||||
|
return (task as any)._state;
|
||||||
|
},
|
||||||
|
set: (newState: string) => {
|
||||||
|
(task as any)._state = newState;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return delegate.scheduleTask(target, task);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.run(() => {
|
||||||
|
const id = setTimeout(() => {}, 0);
|
||||||
|
testZoneSpec.tick();
|
||||||
|
clearTimeout(id);
|
||||||
|
// This is a hack way to test the timerId cache is cleaned or not
|
||||||
|
// since the tasksByHandleId cache is an internal variable held by
|
||||||
|
// zone.js timer patch, if the cache is not cleared, the code in `timer.ts`
|
||||||
|
// will call `task.state` one more time to check whether to clear the
|
||||||
|
// task or not, so here we use this count to check the issue is fixed or not
|
||||||
|
// For details, please refer to https://github.com/angular/angular/issues/40387
|
||||||
|
expect(taskSpy.calls.count()).toEqual(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
it('should pass arguments to setImmediate', ifEnvSupports('setImmediate', () => {
|
it('should pass arguments to setImmediate', ifEnvSupports('setImmediate', () => {
|
||||||
fakeAsyncTestZone.run(() => {
|
fakeAsyncTestZone.run(() => {
|
||||||
let value = 'genuine value';
|
let value = 'genuine value';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user