Skip to content

Commit a308979

Browse files
Implement console.time, console.timeEnd, console.timeLog, console.count, and console.countReset
These methods were previously stubbed as no-ops. Developers using console.time() for profiling would silently get no output, which is confusing and makes debugging harder. The implementation follows the WHATWG Console spec: - console.time(label) starts a named timer - console.timeEnd(label) logs elapsed time and removes the timer - console.timeLog(label, ...data) logs elapsed time without stopping - console.count(label) logs how many times it has been called - console.countReset(label) resets a counter Uses nativePerformanceNow for high-resolution timing when available, with Date.now as a fallback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8bac1df commit a308979

2 files changed

Lines changed: 252 additions & 4 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @fantom_mode *
10+
*/
11+
12+
const LOG_LEVELS = {
13+
trace: 0,
14+
info: 1,
15+
warn: 2,
16+
error: 3,
17+
};
18+
19+
describe('console.time / console.timeEnd / console.timeLog', () => {
20+
let originalNativeLoggingHook;
21+
let logFn;
22+
23+
beforeEach(() => {
24+
originalNativeLoggingHook = global.nativeLoggingHook;
25+
logFn = global.nativeLoggingHook = jest.fn();
26+
});
27+
28+
afterEach(() => {
29+
global.nativeLoggingHook = originalNativeLoggingHook;
30+
});
31+
32+
it('should log elapsed time on timeEnd', () => {
33+
console.time('test');
34+
console.timeEnd('test');
35+
36+
expect(logFn).toHaveBeenCalledTimes(1);
37+
const message = logFn.mock.calls[0][0];
38+
expect(message).toMatch(/^test: \d+(\.\d+)?ms$/);
39+
expect(logFn.mock.calls[0][1]).toBe(LOG_LEVELS.info);
40+
});
41+
42+
it('should use "default" label when none is provided', () => {
43+
console.time();
44+
console.timeEnd();
45+
46+
const message = logFn.mock.calls[0][0];
47+
expect(message).toMatch(/^default: \d+(\.\d+)?ms$/);
48+
});
49+
50+
it('should warn when starting a timer that already exists', () => {
51+
console.time('dup');
52+
console.time('dup');
53+
54+
expect(logFn).toHaveBeenCalledWith(
55+
'Timer "dup" already exists',
56+
LOG_LEVELS.warn,
57+
);
58+
59+
// Clean up
60+
console.timeEnd('dup');
61+
});
62+
63+
it('should warn when ending a timer that does not exist', () => {
64+
console.timeEnd('nonexistent');
65+
66+
expect(logFn).toHaveBeenCalledWith(
67+
'Timer "nonexistent" does not exist',
68+
LOG_LEVELS.warn,
69+
);
70+
});
71+
72+
it('should log elapsed time with timeLog without stopping the timer', () => {
73+
console.time('ongoing');
74+
console.timeLog('ongoing');
75+
console.timeLog('ongoing');
76+
console.timeEnd('ongoing');
77+
78+
// timeLog called twice + timeEnd called once = 3 info logs
79+
expect(logFn).toHaveBeenCalledTimes(3);
80+
for (let i = 0; i < 3; i++) {
81+
expect(logFn.mock.calls[i][0]).toMatch(/^ongoing: \d+(\.\d+)?ms/);
82+
}
83+
});
84+
85+
it('should warn when calling timeLog on a nonexistent timer', () => {
86+
console.timeLog('ghost');
87+
88+
expect(logFn).toHaveBeenCalledWith(
89+
'Timer "ghost" does not exist',
90+
LOG_LEVELS.warn,
91+
);
92+
});
93+
94+
it('should support multiple concurrent timers', () => {
95+
console.time('a');
96+
console.time('b');
97+
console.timeEnd('a');
98+
console.timeEnd('b');
99+
100+
expect(logFn).toHaveBeenCalledTimes(2);
101+
expect(logFn.mock.calls[0][0]).toMatch(/^a: /);
102+
expect(logFn.mock.calls[1][0]).toMatch(/^b: /);
103+
});
104+
});
105+
106+
describe('console.count / console.countReset', () => {
107+
let originalNativeLoggingHook;
108+
let logFn;
109+
110+
beforeEach(() => {
111+
originalNativeLoggingHook = global.nativeLoggingHook;
112+
logFn = global.nativeLoggingHook = jest.fn();
113+
});
114+
115+
afterEach(() => {
116+
global.nativeLoggingHook = originalNativeLoggingHook;
117+
});
118+
119+
it('should increment and log the count', () => {
120+
console.count('clicks');
121+
console.count('clicks');
122+
console.count('clicks');
123+
124+
expect(logFn).toHaveBeenCalledTimes(3);
125+
expect(logFn.mock.calls[0][0]).toBe('clicks: 1');
126+
expect(logFn.mock.calls[1][0]).toBe('clicks: 2');
127+
expect(logFn.mock.calls[2][0]).toBe('clicks: 3');
128+
});
129+
130+
it('should use "default" label when none is provided', () => {
131+
console.count();
132+
133+
expect(logFn.mock.calls[0][0]).toMatch(/^default: \d+$/);
134+
});
135+
136+
it('should reset the count', () => {
137+
console.count('resets');
138+
console.count('resets');
139+
console.countReset('resets');
140+
console.count('resets');
141+
142+
expect(logFn.mock.calls[2][0]).toBe('resets: 1');
143+
});
144+
145+
it('should warn when resetting a nonexistent counter', () => {
146+
console.countReset('nope');
147+
148+
expect(logFn).toHaveBeenCalledWith(
149+
'Count for "nope" does not exist',
150+
LOG_LEVELS.warn,
151+
);
152+
});
153+
154+
it('should track separate labels independently', () => {
155+
console.count('a');
156+
console.count('b');
157+
console.count('a');
158+
159+
expect(logFn.mock.calls[0][0]).toBe('a: 1');
160+
expect(logFn.mock.calls[1][0]).toBe('b: 1');
161+
expect(logFn.mock.calls[2][0]).toBe('a: 2');
162+
});
163+
});

packages/polyfills/console.js

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,90 @@ function consoleAssertPolyfill(expression, label) {
571571

572572
function stub() {}
573573

574+
// Use high-resolution timer if available, fall back to Date.now
575+
var now = global.nativePerformanceNow || Date.now;
576+
577+
// console.time / console.timeLog / console.timeEnd
578+
// https://console.spec.whatwg.org/#timing
579+
var timerTable = {};
580+
581+
function consoleTimePolyfill(label) {
582+
var name = label === undefined ? 'default' : '' + label;
583+
if (timerTable[name] !== undefined) {
584+
global.nativeLoggingHook(
585+
'Timer "' + name + '" already exists',
586+
LOG_LEVELS.warn,
587+
);
588+
return;
589+
}
590+
timerTable[name] = now();
591+
}
592+
593+
function consoleTimeEndPolyfill(label) {
594+
var name = label === undefined ? 'default' : '' + label;
595+
var startTime = timerTable[name];
596+
if (startTime === undefined) {
597+
global.nativeLoggingHook(
598+
'Timer "' + name + '" does not exist',
599+
LOG_LEVELS.warn,
600+
);
601+
return;
602+
}
603+
delete timerTable[name];
604+
var elapsed = now() - startTime;
605+
global.nativeLoggingHook(name + ': ' + elapsed + 'ms', LOG_LEVELS.info);
606+
}
607+
608+
function consoleTimeLogPolyfill(label) {
609+
var name = label === undefined ? 'default' : '' + label;
610+
var startTime = timerTable[name];
611+
if (startTime === undefined) {
612+
global.nativeLoggingHook(
613+
'Timer "' + name + '" does not exist',
614+
LOG_LEVELS.warn,
615+
);
616+
return;
617+
}
618+
var elapsed = now() - startTime;
619+
var extra =
620+
arguments.length > 1
621+
? ' ' +
622+
Array.prototype.slice
623+
.call(arguments, 1)
624+
.map(function (arg) {
625+
return inspect(arg, {depth: 10});
626+
})
627+
.join(' ')
628+
: '';
629+
global.nativeLoggingHook(
630+
name + ': ' + elapsed + 'ms' + extra,
631+
LOG_LEVELS.info,
632+
);
633+
}
634+
635+
// console.count / console.countReset
636+
// https://console.spec.whatwg.org/#counting
637+
var countTable = {};
638+
639+
function consoleCountPolyfill(label) {
640+
var name = label === undefined ? 'default' : '' + label;
641+
var count = (countTable[name] || 0) + 1;
642+
countTable[name] = count;
643+
global.nativeLoggingHook(name + ': ' + count, LOG_LEVELS.info);
644+
}
645+
646+
function consoleCountResetPolyfill(label) {
647+
var name = label === undefined ? 'default' : '' + label;
648+
if (countTable[name] === undefined) {
649+
global.nativeLoggingHook(
650+
'Count for "' + name + '" does not exist',
651+
LOG_LEVELS.warn,
652+
);
653+
return;
654+
}
655+
countTable[name] = 0;
656+
}
657+
574658
// https://developer.chrome.com/docs/devtools/console/api#createtask
575659
function consoleCreateTaskStub() {
576660
return {run: cb => cb()};
@@ -587,11 +671,12 @@ if (global.nativeLoggingHook) {
587671
}
588672

589673
global.console = {
590-
time: stub,
591-
timeEnd: stub,
674+
time: consoleTimePolyfill,
675+
timeEnd: consoleTimeEndPolyfill,
676+
timeLog: consoleTimeLogPolyfill,
592677
timeStamp: stub,
593-
count: stub,
594-
countReset: stub,
678+
count: consoleCountPolyfill,
679+
countReset: consoleCountResetPolyfill,
595680
createTask: consoleCreateTaskStub,
596681
...(originalConsole ?? {}),
597682
error: getNativeLogFunction(LOG_LEVELS.error),

0 commit comments

Comments
 (0)