feat(inspector): render params and durations in log (#5489)

This commit is contained in:
Pavel Feldman 2021-02-17 22:10:13 -08:00 committed by GitHub
parent da135c2abb
commit 15833ee039
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 129 additions and 51 deletions

View file

@ -219,6 +219,7 @@ export class DispatcherConnection {
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote); rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote);
this.onmessage({ id, error: serializeError(e) }); this.onmessage({ id, error: serializeError(e) });
} finally { } finally {
callMetadata.endTime = monotonicTime();
if (sdkObject) if (sdkObject)
await sdkObject.instrumentation.onAfterCall(sdkObject, callMetadata); await sdkObject.instrumentation.onAfterCall(sdkObject, callMetadata);
} }

View file

@ -34,6 +34,8 @@ export type CallMetadata = {
id: number; id: number;
startTime: number; startTime: number;
endTime: number; endTime: number;
pauseStartTime?: number;
pauseEndTime?: number;
type: string; type: string;
method: string; method: string;
params: any; params: any;

View file

@ -109,7 +109,6 @@ export class ProgressController {
throw e; throw e;
} finally { } finally {
clearTimeout(timer); clearTimeout(timer);
this.metadata.endTime = monotonicTime();
} }
} }

View file

@ -72,12 +72,15 @@ export class InspectorController implements InstrumentationListener {
const info = metadata.params.info; const info = metadata.params.info;
switch (info.phase) { switch (info.phase) {
case 'before': case 'before':
metadata.endTime = 0;
// Fall through. // Fall through.
case 'log': case 'log':
return; return;
case 'after': case 'after':
metadata = this._waitOperations.get(info.waitId)!; const originalMetadata = this._waitOperations.get(info.waitId)!;
originalMetadata.endTime = metadata.endTime;
this._waitOperations.delete(info.waitId); this._waitOperations.delete(info.waitId);
metadata = originalMetadata;
break; break;
} }
} }

View file

@ -36,6 +36,11 @@ export type CallLog = {
status: 'in-progress' | 'done' | 'error' | 'paused'; status: 'in-progress' | 'done' | 'error' | 'paused';
error?: string; error?: string;
reveal?: boolean; reveal?: boolean;
duration?: number;
params: {
url?: string,
selector?: string,
};
}; };
export type SourceHighlight = { export type SourceHighlight = {

View file

@ -31,7 +31,7 @@ import { RecorderApp } from './recorder/recorderApp';
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation'; import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
import { Point } from '../../common/types'; import { Point } from '../../common/types';
import { CallLog, EventData, Mode, Source, UIState } from './recorder/recorderTypes'; import { CallLog, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
import { isUnderTest } from '../../utils/utils'; import { isUnderTest, monotonicTime } from '../../utils/utils';
type BindingSource = { frame: Frame, page: Page }; type BindingSource = { frame: Frame, page: Page };
@ -210,6 +210,7 @@ export class RecorderSupplement {
this._pausedCallsMetadata.set(metadata, f); this._pausedCallsMetadata.set(metadata, f);
}); });
this._recorderApp!.setPaused(true); this._recorderApp!.setPaused(true);
metadata.pauseStartTime = monotonicTime();
this._updateUserSources(); this._updateUserSources();
this.updateCallLog([metadata]); this.updateCallLog([metadata]);
return result; return result;
@ -219,8 +220,11 @@ export class RecorderSupplement {
this._pauseOnNextStatement = step; this._pauseOnNextStatement = step;
this._recorderApp?.setPaused(false); this._recorderApp?.setPaused(false);
for (const callback of this._pausedCallsMetadata.values()) const endTime = monotonicTime();
for (const [metadata, callback] of this._pausedCallsMetadata) {
metadata.pauseEndTime = endTime;
callback(); callback();
}
this._pausedCallsMetadata.clear(); this._pausedCallsMetadata.clear();
this._updateUserSources(); this._updateUserSources();
@ -418,7 +422,21 @@ export class RecorderSupplement {
status = 'paused'; status = 'paused';
if (metadata.error) if (metadata.error)
status = 'error'; status = 'error';
logs.push({ id: metadata.id, messages: metadata.log, title, status, error: metadata.error }); const params = {
url: metadata.params?.url,
selector: metadata.params?.selector,
};
let duration = metadata.endTime ? metadata.endTime - metadata.startTime : undefined;
if (duration && metadata.pauseStartTime && metadata.pauseEndTime)
duration -= (metadata.pauseEndTime - metadata.pauseStartTime);
logs.push({
id: metadata.id,
messages: metadata.log,
title, status,
error: metadata.error,
params,
duration
});
} }
this._recorderApp?.updateCallLogs(logs); this._recorderApp?.updateCallLogs(logs);
} }

View file

@ -76,3 +76,16 @@
.call-log .codicon-error { .call-log .codicon-error {
color: red; color: red;
} }
.call-log-url {
color: var(--blue);
}
.call-log-selector {
color: var(--orange);
}
.call-log-time {
margin-left: 4px;
color: var(--gray);
}

View file

@ -22,7 +22,9 @@ export function exampleCallLog(): CallLog[] {
'id': 3, 'id': 3,
'messages': [], 'messages': [],
'title': 'newPage', 'title': 'newPage',
'status': 'done' 'status': 'done',
'duration': 100,
'params': {}
}, },
{ {
'id': 4, 'id': 4,
@ -30,7 +32,11 @@ export function exampleCallLog(): CallLog[] {
'navigating to "https://github.com/microsoft", waiting until "load"', 'navigating to "https://github.com/microsoft", waiting until "load"',
], ],
'title': 'goto', 'title': 'goto',
'status': 'done' 'status': 'done',
'params': {
'url': 'https://github.com/microsoft'
},
'duration': 1100,
}, },
{ {
'id': 5, 'id': 5,
@ -47,7 +53,10 @@ export function exampleCallLog(): CallLog[] {
' performing click action' ' performing click action'
], ],
'title': 'click', 'title': 'click',
'status': 'paused' 'status': 'paused',
'params': {
'selector': 'input[aria-label="Find a repository…"]'
},
}, },
{ {
'id': 6, 'id': 6,
@ -56,7 +65,9 @@ export function exampleCallLog(): CallLog[] {
], ],
'error': 'Error occured', 'error': 'Error occured',
'title': 'error', 'title': 'error',
'status': 'error' 'status': 'error',
'params': {
},
}, },
]; ];
} }

View file

@ -17,6 +17,7 @@
import './callLog.css'; import './callLog.css';
import * as React from 'react'; import * as React from 'react';
import type { CallLog } from '../../server/supplements/recorder/recorderTypes'; import type { CallLog } from '../../server/supplements/recorder/recorderTypes';
import { msToString } from '../uiUtils';
export interface CallLogProps { export interface CallLogProps {
log: CallLog[] log: CallLog[]
@ -46,7 +47,10 @@ export const CallLogView: React.FC<CallLogProps> = ({
setExpandOverrides(newOverrides); setExpandOverrides(newOverrides);
}}></span> }}></span>
{ callLog.title } { callLog.title }
{ callLog.params.url ? <span>(<span className='call-log-url'>{callLog.params.url}</span>)</span> : undefined }
{ callLog.params.selector ? <span>(<span className='call-log-selector'>{callLog.params.selector}</span>)</span> : undefined }
<span className={'codicon ' + iconClass(callLog)}></span> <span className={'codicon ' + iconClass(callLog)}></span>
{ typeof callLog.duration === 'number' ? <span className='call-log-time'> {msToString(callLog.duration)}</span> : undefined}
</div> </div>
{ (isExpanded ? callLog.messages : []).map((message, i) => { { (isExpanded ? callLog.messages : []).map((message, i) => {
return <div className='call-log-message' key={i}> return <div className='call-log-message' key={i}>

View file

@ -72,29 +72,3 @@ export const Expandable: React.FunctionComponent<{
{ expanded && <div style={{ display: 'flex', flex: 'auto', margin: '5px 0 5px 20px' }}>{body}</div> } { expanded && <div style={{ display: 'flex', flex: 'auto', margin: '5px 0 5px 20px' }}>{body}</div> }
</div>; </div>;
}; };
export function msToString(ms: number): string {
if (!isFinite(ms))
return '-';
if (ms === 0)
return '0';
if (ms < 1000)
return ms.toFixed(0) + 'ms';
const seconds = ms / 1000;
if (seconds < 60)
return seconds.toFixed(1) + 's';
const minutes = seconds / 60;
if (minutes < 60)
return minutes.toFixed(1) + 'm';
const hours = minutes / 60;
if (hours < 24)
return hours.toFixed(1) + 'h';
const days = hours / 24;
return days.toFixed(1) + 'd';
}

View file

@ -18,7 +18,8 @@ import { ActionEntry } from '../../../cli/traceViewer/traceModel';
import { Boundaries, Size } from '../geometry'; import { Boundaries, Size } from '../geometry';
import './snapshotTab.css'; import './snapshotTab.css';
import * as React from 'react'; import * as React from 'react';
import { msToString, useMeasure } from './helpers'; import { useMeasure } from './helpers';
import { msToString } from '../../uiUtils';
export const SnapshotTab: React.FunctionComponent<{ export const SnapshotTab: React.FunctionComponent<{
actionEntry: ActionEntry | undefined, actionEntry: ActionEntry | undefined,

View file

@ -19,7 +19,8 @@ import { ContextEntry, InterestingPageEvent, ActionEntry, trace } from '../../..
import './timeline.css'; import './timeline.css';
import { Boundaries } from '../geometry'; import { Boundaries } from '../geometry';
import * as React from 'react'; import * as React from 'react';
import { msToString, useMeasure } from './helpers'; import { useMeasure } from './helpers';
import { msToString } from '../../uiUtils';
type TimelineBar = { type TimelineBar = {
entry?: ActionEntry; entry?: ActionEntry;

41
src/web/uiUtils.ts Normal file
View file

@ -0,0 +1,41 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export function msToString(ms: number): string {
if (!isFinite(ms))
return '-';
if (ms === 0)
return '0';
if (ms < 1000)
return ms.toFixed(0) + 'ms';
const seconds = ms / 1000;
if (seconds < 60)
return seconds.toFixed(1) + 's';
const minutes = seconds / 60;
if (minutes < 60)
return minutes.toFixed(1) + 'm';
const hours = minutes / 60;
if (hours < 24)
return hours.toFixed(1) + 'h';
const days = hours / 24;
return days.toFixed(1) + 'd';
}

View file

@ -146,8 +146,8 @@ describe('pause', (suite, { mode }) => {
await recorderPage.click('[title="Resume"]'); await recorderPage.click('[title="Resume"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")'); await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([ expect(await sanitizeLog(recorderPage)).toEqual([
'pause', 'pause- XXms',
'click', 'click(button)- XXms',
'pause', 'pause',
]); ]);
await recorderPage.click('[title="Resume"]'); await recorderPage.click('[title="Resume"]');
@ -168,9 +168,9 @@ describe('pause', (suite, { mode }) => {
await recorderPage.click('[title="Resume"]'); await recorderPage.click('[title="Resume"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")'); await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([ expect(await sanitizeLog(recorderPage)).toEqual([
'pause', 'pause- XXms',
'waitForEvent()', 'waitForEvent(console)- XXms',
'click', 'click(button)- XXms',
'pause', 'pause',
]); ]);
await recorderPage.click('[title="Resume"]'); await recorderPage.click('[title="Resume"]');
@ -187,10 +187,10 @@ describe('pause', (suite, { mode }) => {
await recorderPage.click('[title="Resume"]'); await recorderPage.click('[title="Resume"]');
await recorderPage.waitForSelector('.source-line-error'); await recorderPage.waitForSelector('.source-line-error');
expect(await sanitizeLog(recorderPage)).toEqual([ expect(await sanitizeLog(recorderPage)).toEqual([
'pause', 'pause- XXms',
'isChecked', 'isChecked(button)- XXms',
'checking \"checked\" state of \"button\"', 'checking \"checked\" state of \"button\"',
'selector resolved to <button onclick=\"console.log()\">Submit</button>', 'selector resolved to <button onclick=\"console.log(1)\">Submit</button>',
'Not a checkbox or radio button', 'Not a checkbox or radio button',
]); ]);
const error = await scriptPromise; const error = await scriptPromise;
@ -199,10 +199,15 @@ describe('pause', (suite, { mode }) => {
}); });
async function sanitizeLog(recorderPage: Page): Promise<string[]> { async function sanitizeLog(recorderPage: Page): Promise<string[]> {
const text = await recorderPage.innerText('.call-log'); const results = [];
return text.split('\n').filter(l => { for (const entry of await recorderPage.$$('.call-log-call')) {
return l !== 'element is not stable - waiting...'; const header = await (await (await entry.$('.call-log-call-header')).textContent()).replace(/— \d+(ms|s)/, '- XXms');
}).map(l => { results.push(header);
return l.replace(/\(.*\)/, '()'); results.push(...await entry.$$eval('.call-log-message', ee => ee.map(e => e.textContent)));
}); const errorElement = await entry.$('.call-log-error');
const error = errorElement ? await errorElement.textContent() : undefined;
if (error)
results.push(error);
}
return results;
} }