Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions test/parallel/test-util-throttle-timing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Flags: --expose-internals
'use strict';

const common = require('../common');
const assert = require('node:assert');
const { mock } = require('node:test');
const { setImmediate } = require('node:timers/promises');
const { internalBinding } = require('internal/test/binding');

// Throttle uses the libuv clock as well as timers. Control both before loading
// the implementation, which captures setTimeout and clearTimeout.
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
mock.method(internalBinding('timers'), 'getLibuvNow', () => Date.now());
const { throttle } = require('node:util');

process.on('unhandledRejection', common.mustNotCall());

(async () => {
{
const values = [];
const times = [];
const start = Date.now();
const throttled = throttle(common.mustCall(function(value) {
assert.strictEqual(this, throttled);
values.push(value);
times.push(Date.now() - start);
return value;
}, 5), 2, 40);

assert.strictEqual(typeof throttled.cancel, 'function');
assert.strictEqual(typeof throttled.hasImmediateCapacity, 'function');
assert.strictEqual(typeof throttled.ref, 'function');
assert.strictEqual(typeof throttled.unref, 'function');
assert.strictEqual(throttled.pending, null);
assert.strictEqual(throttled.pendingCount, 0);
assert.strictEqual(throttled.activeCount, 0);
assert.strictEqual(throttled.hasImmediateCapacity(), true);

const first = throttled(1);
const second = throttled(2);
const third = throttled(3);
const fourth = throttled(4);
const fifth = throttled(5);

assert(first instanceof Promise);
assert(second instanceof Promise);
assert.notStrictEqual(first, second);
assert.deepStrictEqual(values, [1, 2]);
assert.strictEqual(throttled.hasImmediateCapacity(), false);
assert.strictEqual(throttled.pending, fifth);
assert.strictEqual(throttled.pendingCount, 3);
assert.strictEqual(throttled.unref(), throttled);
assert.strictEqual(throttled.ref(), throttled);

mock.timers.tick(39);
assert.deepStrictEqual(values, [1, 2]);
mock.timers.tick(1);
assert.deepStrictEqual(values, [1, 2, 3, 4]);
assert.strictEqual(throttled.pendingCount, 1);
mock.timers.tick(39);
assert.deepStrictEqual(values, [1, 2, 3, 4]);
mock.timers.tick(1);

assert.deepStrictEqual(
await Promise.all([first, second, third, fourth, fifth]),
[1, 2, 3, 4, 5],
);
assert.deepStrictEqual(values, [1, 2, 3, 4, 5]);
assert.strictEqual(throttled.pending, null);
assert.strictEqual(throttled.pendingCount, 0);
assert.strictEqual(throttled.activeCount, 0);
assert.deepStrictEqual(times, [0, 0, 40, 40, 80]);
}

{
const values = [];
const throttled = throttle(common.mustCall((value) => {
values.push(value);
return value;
}, 2), 1, 20, { maxPending: 1 });
const first = throttled(1);
const second = throttled(2);
const dropped = throttled(3);

assert.deepStrictEqual(values, [1]);
assert.strictEqual(throttled.pendingCount, 1);
await setImmediate();
await assert.rejects(dropped, { code: 'ERR_THROTTLED' });
mock.timers.tick(19);
assert.deepStrictEqual(values, [1]);
mock.timers.tick(1);
assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]);
assert.deepStrictEqual(values, [1, 2]);
}

{
const values = [];
const throttled = throttle(common.mustCall((value) => {
values.push(value);
return value;
}, 3), 2, 30, { overflow: 'drop' });
const first = throttled(1);
const second = throttled(2);
await assert.rejects(throttled(3), { code: 'ERR_THROTTLED' });
assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]);

mock.timers.tick(29);
assert.strictEqual(throttled.hasImmediateCapacity(), false);
await assert.rejects(throttled(4), { code: 'ERR_THROTTLED' });
mock.timers.tick(1);
assert.strictEqual(throttled.hasImmediateCapacity(), true);
assert.strictEqual(await throttled(5), 5);
assert.deepStrictEqual(values, [1, 2, 5]);
}

// A rolling window releases one slot at a time when timers run on schedule.
// If dispatch is delayed until both slots expire, both calls may run together.
for (const delayed of [false, true]) {
const times = [];
const start = Date.now();
const throttled = throttle(common.mustCall((value) => {
times.push(Date.now() - start);
return value;
}, 4), 2, 80, { strict: true });

const first = throttled(1);
mock.timers.tick(40);
const second = throttled(2);
const third = throttled(3);
const fourth = throttled(4);

mock.timers.tick(39);
assert.deepStrictEqual(times, [0, 40]);
if (delayed) {
mock.timers.tick(41);
} else {
mock.timers.tick(1);
assert.deepStrictEqual(times, [0, 40, 80]);
mock.timers.tick(39);
assert.deepStrictEqual(times, [0, 40, 80]);
mock.timers.tick(1);
}

assert.deepStrictEqual(
await Promise.all([first, second, third, fourth]),
[1, 2, 3, 4],
);
assert.deepStrictEqual(times, delayed ? [0, 40, 120, 120] : [0, 40, 80, 120]);
}

{
let recursive;
const values = [];
const throttled = throttle(common.mustCall((value) => {
values.push(value);
if (value === 1) recursive = throttled(2);
return value;
}, 2), 1, 20);

const first = throttled(1);
assert.deepStrictEqual(values, [1]);
assert.strictEqual(throttled.pending, recursive);
assert.strictEqual(throttled.pendingCount, 1);
mock.timers.tick(19);
assert.deepStrictEqual(values, [1]);
mock.timers.tick(1);
assert.deepStrictEqual(await Promise.all([first, recursive]), [1, 2]);
assert.deepStrictEqual(values, [1, 2]);
}
})().then(common.mustCall()).finally(() => mock.reset());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
})().then(common.mustCall()).finally(() => mock.reset());
})().finally(() => mock.reset()).then(common.mustCall());

124 changes: 11 additions & 113 deletions test/parallel/test-util-throttle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
const common = require('../common');
const assert = require('node:assert');
const { createHook } = require('node:async_hooks');
const { setImmediate, setTimeout } = require('node:timers/promises');
const { setImmediate } = require('node:timers/promises');
const { throttle } = require('node:util');
const { TIMEOUT_MAX } = require('internal/timers');

Expand Down Expand Up @@ -95,55 +95,9 @@ throttle(() => {}, 1, 1, {
);
}

// Keep windows open across synchronous assertions, even if the process is
// descheduled. Window expiration is covered in test-util-throttle-timing.js.
(async () => {
{
const values = [];
const times = [];
const start = Date.now();
const throttled = throttle(common.mustCall(function(value) {
assert.strictEqual(this, throttled);
values.push(value);
times.push(Date.now() - start);
return value;
}, 5), 2, 40);

assert.strictEqual(typeof throttled.cancel, 'function');
assert.strictEqual(typeof throttled.hasImmediateCapacity, 'function');
assert.strictEqual(typeof throttled.ref, 'function');
assert.strictEqual(typeof throttled.unref, 'function');
assert.strictEqual(throttled.pending, null);
assert.strictEqual(throttled.pendingCount, 0);
assert.strictEqual(throttled.activeCount, 0);
assert.strictEqual(throttled.hasImmediateCapacity(), true);

const first = throttled(1);
const second = throttled(2);
const third = throttled(3);
const fourth = throttled(4);
const fifth = throttled(5);

assert(first instanceof Promise);
assert(second instanceof Promise);
assert.notStrictEqual(first, second);
assert.deepStrictEqual(values, [1, 2]);
assert.strictEqual(throttled.hasImmediateCapacity(), false);
assert.strictEqual(throttled.pending, fifth);
assert.strictEqual(throttled.pendingCount, 3);
assert.strictEqual(throttled.unref(), throttled);
assert.strictEqual(throttled.ref(), throttled);

assert.deepStrictEqual(
await Promise.all([first, second, third, fourth, fifth]),
[1, 2, 3, 4, 5],
);
assert.deepStrictEqual(values, [1, 2, 3, 4, 5]);
assert.strictEqual(throttled.pending, null);
assert.strictEqual(throttled.pendingCount, 0);
assert.strictEqual(throttled.activeCount, 0);
assert(times[2] - times[0] >= 30);
assert(times[4] - times[2] >= 30);
}

{
let running = 0;
let maxRunning = 0;
Expand Down Expand Up @@ -220,24 +174,6 @@ throttle(() => {}, 1, 1, {
assert.strictEqual(throttled.activeCount, 0);
}

{
const values = [];
const throttled = throttle(common.mustCall((value) => {
values.push(value);
return value;
}, 2), 1, 20, { maxPending: 1 });
const first = throttled(1);
const second = throttled(2);
const dropped = throttled(3);

assert.deepStrictEqual(values, [1]);
assert.strictEqual(throttled.pendingCount, 1);
await setImmediate();
await assert.rejects(dropped, { code: 'ERR_THROTTLED' });
assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]);
assert.deepStrictEqual(values, [1, 2]);
}

{
let timeoutCount = 0;
const hook = createHook({
Expand All @@ -249,7 +185,7 @@ throttle(() => {}, 1, 1, {
const throttled = throttle(common.mustCall((value) => {
values.push(value);
return value;
}, 3), 2, 30, { overflow: 'drop' });
}, 3), 2, TIMEOUT_MAX, { overflow: 'drop' });

hook.enable();
const first = throttled(1);
Expand All @@ -265,37 +201,14 @@ throttle(() => {}, 1, 1, {
await assert.rejects(dropped, { code: 'ERR_THROTTLED' });
assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]);

await setTimeout(30);
throttled.cancel();
assert.strictEqual(await throttled(4), 4);
assert.deepStrictEqual(values, [1, 2, 4]);
}

{
const times = [];
const start = Date.now();
const throttled = throttle(common.mustCall((value) => {
times.push(Date.now() - start);
return value;
}, 4), 2, 80, { strict: true });

const first = throttled(1);
await setTimeout(40);
const second = throttled(2);
const third = throttled(3);
const fourth = throttled(4);

assert.deepStrictEqual(
await Promise.all([first, second, third, fourth]),
[1, 2, 3, 4],
);
assert(times[2] - times[0] >= 65);
assert(times[3] - times[1] >= 65);
assert(times[3] - times[2] >= 25);
}

{
const reason = new Error('cancelled');
const throttled = throttle(common.mustCall((value) => value, 2), 1, 100);
const throttled = throttle(common.mustCall((value) => value, 2), 1, TIMEOUT_MAX);
const first = throttled(1);
const second = throttled(2);
const third = throttled(3);
Expand All @@ -322,7 +235,7 @@ throttle(() => {}, 1, 1, {
{
const reason = new Error('stop');
const controller = new AbortController();
const throttled = throttle(common.mustCall((value) => value), 1, 100, {
const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX, {
signal: controller.signal,
});
const first = throttled(1);
Expand Down Expand Up @@ -363,23 +276,6 @@ throttle(() => {}, 1, 1, {
await assert.rejects(throttled(), (error) => error === expected);
}

{
let recursive;
const values = [];
const throttled = throttle(common.mustCall((value) => {
values.push(value);
if (value === 1) recursive = throttled(2);
return value;
}, 2), 1, 20);

const first = throttled(1);
assert.deepStrictEqual(values, [1]);
assert.strictEqual(throttled.pending, recursive);
assert.strictEqual(throttled.pendingCount, 1);
assert.deepStrictEqual(await Promise.all([first, recursive]), [1, 2]);
assert.deepStrictEqual(values, [1, 2]);
}

{
function original(first, second) {
return first + second;
Expand Down Expand Up @@ -409,7 +305,7 @@ throttle(() => {}, 1, 1, {
if (type === 'Timeout') timeoutCount++;
},
});
const throttled = throttle(common.mustCall((value) => value), 1, 100);
const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX);

hook.enable();
assert.strictEqual(throttled.hasImmediateCapacity(), true);
Expand All @@ -430,7 +326,7 @@ throttle(() => {}, 1, 1, {
if (type === 'Timeout') timeoutCount++;
},
});
const throttled = throttle(common.mustCall((value) => value), 1, 100);
const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX);
hook.enable();
const first = throttled(1);
const second = throttled(2);
Expand All @@ -440,6 +336,8 @@ throttle(() => {}, 1, 1, {
const thirdRejection = assert.rejects(third, { code: 'ABORT_ERR' });

assert.strictEqual(timeoutCount, 1);
assert.strictEqual(throttled.unref(), throttled);
assert.strictEqual(throttled.ref(), throttled);
throttled.cancel();
assert.strictEqual(await first, 1);
await Promise.all([secondRejection, thirdRejection]);
Expand Down
Loading