blob: 8c2e856845ec3908b174e341124c409b983c0c41 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* 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.
*/
import {assertDefined} from 'common/assert_utils';
import {FileUtils} from 'common/file_utils';
import {
TimestampConverterUtils,
timestampEqualityTester,
} from 'common/time/test_utils';
import {ProgressListenerStub} from 'messaging/progress_listener_stub';
import {UserWarning} from 'messaging/user_warning';
import {
CorruptedArchive,
InvalidPerfettoTrace,
NoValidFiles,
PerfettoPacketLoss,
TraceOverridden,
UnsupportedFileFormat,
} from 'messaging/user_warnings';
import {getFixtureFile} from 'test/unit/fixture_utils';
import {TracesUtils} from 'test/unit/traces_utils';
import {UserNotifierChecker} from 'test/unit/user_notifier_checker';
import {TraceType} from 'trace/trace_type';
import {QueryResult} from 'trace_processor/query_result';
import {TraceProcessor} from 'trace_processor/trace_processor';
import {FilesSource} from './files_source';
import {TracePipeline} from './trace_pipeline';
describe('TracePipeline', () => {
let validSfFile: File;
let validWmFile: File;
let shellTransitionFile: File;
let wmTransitionFile: File;
let screenshotFile: File;
let screenRecordingFile: File;
let brMainEntryFile: File;
let brCodenameFile: File;
let brSfFile: File;
let jpgFile: File;
let perfettoFile: File;
let progressListener: ProgressListenerStub;
let tracePipeline: TracePipeline;
let userNotifierChecker: UserNotifierChecker;
beforeAll(async () => {
userNotifierChecker = new UserNotifierChecker();
wmTransitionFile = await getFixtureFile(
'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
);
shellTransitionFile = await getFixtureFile(
'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
);
validSfFile = await getFixtureFile(
'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
);
validWmFile = await getFixtureFile(
'traces/elapsed_and_real_timestamp/WindowManager.pb',
);
screenshotFile = await getFixtureFile('traces/screenshot/screenshot.png');
screenRecordingFile = await getFixtureFile(
'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4',
);
brMainEntryFile = await getFixtureFile(
'bugreports/main_entry.txt',
'main_entry.txt',
);
brCodenameFile = await getFixtureFile(
'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
);
brSfFile = await getFixtureFile(
'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
'FS/data/misc/wmtrace/surface_flinger.bp',
);
jpgFile = await getFixtureFile('invalid_files/winscope_homepage.jpg');
perfettoFile = await getFixtureFile(
'traces/perfetto/layers_trace.perfetto-trace',
'traces/perfetto/layers_trace',
);
});
beforeEach(async () => {
jasmine.addCustomEqualityTester(timestampEqualityTester);
progressListener = new ProgressListenerStub();
spyOn(progressListener, 'onProgressUpdate');
spyOn(progressListener, 'onOperationFinished');
userNotifierChecker.reset();
tracePipeline = new TracePipeline();
});
it('can load valid trace files', async () => {
expect(tracePipeline.getTraces().getSize()).toEqual(0);
await loadFiles([validSfFile, validWmFile], FilesSource.TEST);
await expectLoadResult(2, []);
expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
new RegExp(`${FilesSource.TEST}_`),
);
expect(tracePipeline.getTraces().getSize()).toEqual(2);
const traceEntries = await TracesUtils.extractEntries(
tracePipeline.getTraces(),
);
expect(traceEntries.get(TraceType.WINDOW_MANAGER)?.length).toBeGreaterThan(
0,
);
expect(traceEntries.get(TraceType.SURFACE_FLINGER)?.length).toBeGreaterThan(
0,
);
});
it('can load valid gzipped file and archive', async () => {
expect(tracePipeline.getTraces().getSize()).toEqual(0);
const gzippedFile = await getFixtureFile('archives/WindowManager.pb.gz');
const gzippedArchive = await getFixtureFile(
'archives/WindowManager.zip.gz',
);
await loadFiles([gzippedFile, gzippedArchive], FilesSource.TEST);
await expectLoadResult(2, []);
const traces = tracePipeline.getTraces();
expect(traces.getSize()).toEqual(2);
expect(traces.getTraces(TraceType.WINDOW_MANAGER).length).toEqual(2);
const traceEntries = await TracesUtils.extractEntries(traces);
expect(traceEntries.get(TraceType.WINDOW_MANAGER)?.length).toBeGreaterThan(
0,
);
});
it('can set download archive filename based on files source', async () => {
await loadFiles([validSfFile]);
await expectLoadResult(1, []);
expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
new RegExp('SurfaceFlinger_'),
);
tracePipeline.clear();
await loadFiles([validSfFile, validWmFile], FilesSource.COLLECTED);
await expectLoadResult(2, []);
expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
new RegExp(`${FilesSource.COLLECTED}_`),
);
});
it('can convert illegal uploaded archive filename to legal name for download archive', async () => {
const fileWithIllegalName = await getFixtureFile(
'traces/elapsed_and_real_timestamp/SFtrace(with_illegal_characters).pb',
);
await loadFiles([fileWithIllegalName]);
await expectLoadResult(1, []);
const downloadFilename = tracePipeline.getDownloadArchiveFilename();
expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test(downloadFilename)).toBeTrue();
});
it('detects bugreports and filters out files based on their directory', async () => {
expect(tracePipeline.getTraces().getSize()).toEqual(0);
const bugreportFiles = [
brMainEntryFile,
brCodenameFile,
brSfFile,
await getFixtureFile(
'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
'FS/data/misc/ignored-dir/window_manager.bp',
),
];
const bugreportArchive = new File(
[await FileUtils.createZipArchive(bugreportFiles)],
'bugreport.zip',
);
// Corner case:
// Another file is loaded along the bugreport -> the file must not be ignored
//
// Note:
// The even weirder corner case where two bugreports are loaded at the same time is
// currently not properly handled.
const otherFile = await getFixtureFile(
'traces/elapsed_and_real_timestamp/InputMethodClients.pb',
'would-be-ignored-if-was-in-bugreport-archive/input_method_clients.pb',
);
await loadFiles([bugreportArchive, otherFile]);
await expectLoadResult(2, []);
const traces = tracePipeline.getTraces();
expect(traces.getTrace(TraceType.SURFACE_FLINGER)).toBeDefined();
expect(traces.getTrace(TraceType.WINDOW_MANAGER)).toBeUndefined(); // ignored
expect(traces.getTrace(TraceType.INPUT_METHOD_CLIENTS)).toBeDefined();
});
it('detects bugreports and extracts timezone info, then calculates utc offset', async () => {
const bugreportFiles = [brMainEntryFile, brCodenameFile, brSfFile];
const bugreportArchive = new File(
[await FileUtils.createZipArchive(bugreportFiles)],
'bugreport.zip',
);
await loadFiles([bugreportArchive]);
await expectLoadResult(1, []);
const timestampConverter = tracePipeline.getTimestampConverter();
expect(timestampConverter);
expect(timestampConverter.getUTCOffset()).toEqual('UTC+05:30');
const expectedTimestamp =
TimestampConverterUtils.makeRealTimestampWithUTCOffset(
1659107089102062832n,
);
expect(
timestampConverter.makeTimestampFromMonotonicNs(14500282843n),
).toEqual(expectedTimestamp);
});
it('is robust to corrupted archive', async () => {
const corruptedArchive = await getFixtureFile(
'invalid_files/corrupted_archive.zip',
);
await loadFiles([corruptedArchive]);
await expectLoadResult(0, [
new CorruptedArchive(corruptedArchive),
new NoValidFiles(),
]);
});
it('is robust to invalid trace files', async () => {
const invalidFiles = [jpgFile];
await loadFiles(invalidFiles);
await expectLoadResult(0, [
new UnsupportedFileFormat('winscope_homepage.jpg'),
]);
});
it('notifies for unsupported file uploaded before valid file', async () => {
const invalidFiles = [jpgFile, perfettoFile];
await loadFiles(invalidFiles);
await expectLoadResult(1, [
new UnsupportedFileFormat('winscope_homepage.jpg'),
]);
});
it('notifies for unsupported file uploaded after valid file', async () => {
const invalidFiles = [perfettoFile, jpgFile];
await loadFiles(invalidFiles);
await expectLoadResult(1, [
new UnsupportedFileFormat('winscope_homepage.jpg'),
]);
});
it('is robust to invalid perfetto trace files', async () => {
const invalidFiles = [
await getFixtureFile('invalid_files/invalid_protolog.perfetto-trace'),
];
await loadFiles(invalidFiles);
await expectLoadResult(0, [
new InvalidPerfettoTrace('invalid_protolog.perfetto-trace', [
'Perfetto trace has no Winscope trace entries',
]),
]);
});
it('shows warning for packet loss', async () => {
const file = [
await getFixtureFile('traces/perfetto/layers_trace.perfetto-trace'),
];
const queryResultObj = jasmine.createSpyObj<QueryResult>('result', [
'numRows',
'firstRow',
'waitAllRows',
]);
queryResultObj.numRows.and.returnValue(1);
queryResultObj.firstRow.and.returnValue({value: 2n});
queryResultObj.waitAllRows.and.returnValue(Promise.resolve(queryResultObj));
const spy = spyOn(
TraceProcessor.prototype,
'queryAllRows',
).and.callThrough();
spy
.withArgs(
"select name, value from stats where name = 'traced_buf_trace_writer_packet_loss'",
)
.and.returnValue(Promise.resolve(queryResultObj));
await loadFiles(file);
await expectLoadResult(1, [
new PerfettoPacketLoss('layers_trace.perfetto-trace', 2),
]);
});
it('is robust to mixed valid and invalid trace files', async () => {
expect(tracePipeline.getTraces().getSize()).toEqual(0);
const files = [
jpgFile,
await getFixtureFile('traces/elapsed_timestamp/dump_WindowManager.pb'),
];
await loadFiles(files);
await expectLoadResult(1, [
new UnsupportedFileFormat('winscope_homepage.jpg'),
]);
});
it('can remove traces', async () => {
await loadFiles([validSfFile, validWmFile]);
await expectLoadResult(2, []);
const sfTrace = assertDefined(
tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER),
);
const wmTrace = assertDefined(
tracePipeline.getTraces().getTrace(TraceType.WINDOW_MANAGER),
);
tracePipeline.removeTrace(sfTrace);
await expectLoadResult(1, []);
tracePipeline.removeTrace(wmTrace);
await expectLoadResult(0, []);
});
it('removes constituent traces of transitions trace but keeps for download', async () => {
const files = [wmTransitionFile, wmTransitionFile, shellTransitionFile];
await loadFiles(files);
await expectLoadResult(1, []);
const transitionTrace = assertDefined(
tracePipeline.getTraces().getTrace(TraceType.TRANSITION),
);
tracePipeline.removeTrace(transitionTrace);
await expectLoadResult(0, []);
await loadFiles([wmTransitionFile]);
await expectLoadResult(1, []);
expect(
tracePipeline.getTraces().getTrace(TraceType.WM_TRANSITION),
).toBeDefined();
await expectDownloadResult([
'transition/shell_transition_trace.pb',
'transition/wm_transition_trace.pb',
]);
});
it('removes constituent traces of CUJs trace but keeps for download', async () => {
const files = [
await getFixtureFile(
'traces/elapsed_and_real_timestamp/eventlog.winscope',
),
];
await loadFiles(files);
await expectLoadResult(1, []);
const cujTrace = assertDefined(
tracePipeline.getTraces().getTrace(TraceType.CUJS),
);
tracePipeline.removeTrace(cujTrace);
await expectLoadResult(0, []);
await expectDownloadResult(['eventlog/eventlog.winscope']);
});
it('removes constituent traces of input trace but keeps for download', async () => {
const files = [
await getFixtureFile('traces/perfetto/input-events.perfetto-trace'),
];
await loadFiles(files);
await expectLoadResult(1, []);
const inputTrace = assertDefined(
tracePipeline.getTraces().getTrace(TraceType.INPUT_EVENT_MERGED),
);
tracePipeline.removeTrace(inputTrace);
await expectLoadResult(0, []);
await expectDownloadResult(['input-events.perfetto-trace']);
});
it('gets loaded traces', async () => {
await loadFiles([validSfFile, validWmFile]);
await expectLoadResult(2, []);
const traces = tracePipeline.getTraces();
const actualTraceTypes = new Set(traces.mapTrace((trace) => trace.type));
const expectedTraceTypes = new Set([
TraceType.SURFACE_FLINGER,
TraceType.WINDOW_MANAGER,
]);
expect(actualTraceTypes).toEqual(expectedTraceTypes);
const sfTrace = assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER));
expect(sfTrace.getDescriptors().length).toBeGreaterThan(0);
});
it('gets screenrecording data', async () => {
const files = [screenRecordingFile];
await loadFiles(files);
await expectLoadResult(1, []);
const video = await tracePipeline.getScreenRecordingVideo();
expect(video).toBeDefined();
expect(video?.size).toBeGreaterThan(0);
});
it('gets screenshot data', async () => {
const files = [screenshotFile];
await loadFiles(files);
await expectLoadResult(1, []);
const video = await tracePipeline.getScreenRecordingVideo();
expect(video).toBeDefined();
expect(video?.size).toBeGreaterThan(0);
});
it('prioritizes screenrecording over screenshot data', async () => {
const files = [screenshotFile, screenRecordingFile];
await loadFiles(files);
await expectLoadResult(1, [
new TraceOverridden('screenshot.png', TraceType.SCREEN_RECORDING),
]);
const video = await tracePipeline.getScreenRecordingVideo();
expect(video).toBeDefined();
expect(video?.size).toBeGreaterThan(0);
});
it('creates traces with correct type', async () => {
await loadFiles([validSfFile, validWmFile]);
await expectLoadResult(2, []);
const traces = tracePipeline.getTraces();
traces.forEachTrace((trace, type) => {
expect(trace.type).toEqual(type);
});
});
it('creates zip archive with loaded trace files', async () => {
const files = [
screenRecordingFile,
await getFixtureFile('traces/perfetto/transactions_trace.perfetto-trace'),
];
await loadFiles(files);
await expectLoadResult(2, []);
await expectDownloadResult([
'screen_recording_metadata_v2.mp4',
'transactions_trace.perfetto-trace',
]);
});
it('can be cleared', async () => {
await loadFiles([validSfFile, validWmFile]);
await expectLoadResult(2, []);
tracePipeline.clear();
expect(tracePipeline.getTraces().getSize()).toEqual(0);
});
it('can filter traces without visualization', async () => {
await loadFiles([shellTransitionFile]);
await expectLoadResult(1, []);
tracePipeline.filterTracesWithoutVisualization();
expect(tracePipeline.getTraces().getSize()).toEqual(0);
expect(
tracePipeline.getTraces().getTrace(TraceType.SHELL_TRANSITION),
).toBeUndefined();
});
it('tries to create search trace', async () => {
await loadFiles([perfettoFile]);
const validQuery = 'select ts from surfaceflinger_layers_snapshot';
expect(await tracePipeline.tryCreateSearchTrace(validQuery)).toBeDefined();
expect(await tracePipeline.tryCreateSearchTrace('fail')).toBeUndefined();
});
it('creates screen recording using metadata', async () => {
const screenRecording = await getFixtureFile(
'traces/elapsed_and_real_timestamp/screen_recording_no_metadata.mp4',
);
const metadata = await getFixtureFile(
'traces/elapsed_and_real_timestamp/screen_recording_metadata.json',
);
await loadFiles([screenRecording, metadata]);
await expectLoadResult(1, []);
});
async function loadFiles(
files: File[],
source: FilesSource = FilesSource.TEST,
) {
await tracePipeline.loadFiles(files, source, progressListener);
expect(progressListener.onOperationFinished).toHaveBeenCalled();
await tracePipeline.buildTraces();
}
async function expectLoadResult(
numberOfTraces: number,
expectedWarnings: UserWarning[],
) {
userNotifierChecker.expectAdded(expectedWarnings);
expect(tracePipeline.getTraces().getSize()).toEqual(numberOfTraces);
}
async function expectDownloadResult(expectedArchiveContents: string[]) {
const zipArchive = await tracePipeline.makeZipArchiveWithLoadedTraceFiles();
const actualArchiveContents = (await FileUtils.unzipFile(zipArchive))
.map((file) => file.name)
.sort();
expect(actualArchiveContents).toEqual(expectedArchiveContents);
}
});