• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2025 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {ProgressListener} from 'messaging/progress_listener';
18import {ProxyTracingWarnings} from 'messaging/user_warnings';
19import {UserNotifierChecker} from 'test/unit/user_notifier_checker';
20import {AdbDeviceState} from 'trace_collection/adb/adb_device_connection';
21import {AdbConnectionType} from 'trace_collection/adb_connection_type';
22import {ConnectionState} from 'trace_collection/connection_state';
23import {ConnectionStateListener} from 'trace_collection/connection_state_listener';
24import {MockAdbDeviceConnection} from 'trace_collection/mock/mock_adb_device_connection';
25import {MockAdbHostConnection} from 'trace_collection/mock/mock_adb_host_connection';
26import {AdbFileIdentifier, TraceTarget} from 'trace_collection/trace_target';
27import {UiTraceTarget} from 'trace_collection/ui/ui_trace_target';
28import {UserRequest} from 'trace_collection/user_request';
29import {PerfettoSessionModerator} from './perfetto_session_moderator';
30import {TraceCollectionController} from './trace_collection_controller';
31import {TracingSession} from './tracing_session';
32import {WINSCOPE_BACKUP_DIR} from './winscope_backup_dir';
33
34describe('TraceCollectionController', () => {
35  const listener = jasmine.createSpyObj<
36    ConnectionStateListener & ProgressListener
37  >('ConnectionStateListener', [
38    'onAvailableTracesChange',
39    'onDevicesChange',
40    'onError',
41    'onConnectionStateChange',
42    'onProgressUpdate',
43    'onOperationFinished',
44  ]);
45
46  const mockDevice = new MockAdbDeviceConnection(
47    '35562',
48    'Pixel 6',
49    AdbDeviceState.AVAILABLE,
50    listener,
51  );
52  const mockUserRequest: UserRequest = {
53    target: UiTraceTarget.WINDOW_MANAGER_TRACE,
54    config: [],
55  };
56
57  let controller: TraceCollectionController;
58  let restartSpy: jasmine.Spy;
59  let moveSpy: jasmine.Spy;
60  let runShellCmdSpy: jasmine.Spy;
61
62  beforeEach(() => {
63    restartSpy = spyOn(
64      MockAdbHostConnection.prototype,
65      'restart',
66    ).and.callThrough();
67    moveSpy = spyOn(TracingSession.prototype, 'moveFiles');
68    runShellCmdSpy = spyOn(mockDevice, 'runShellCommand');
69    controller = new TraceCollectionController(
70      AdbConnectionType.MOCK,
71      listener,
72    );
73    resetListener();
74  });
75
76  describe('initialization and destruction:', () => {
77    let hostDestroySpy: jasmine.Spy;
78    let securityTokenSpy: jasmine.Spy;
79
80    beforeEach(() => {
81      hostDestroySpy = spyOn(MockAdbHostConnection.prototype, 'onDestroy');
82      securityTokenSpy = spyOn(
83        MockAdbHostConnection.prototype,
84        'setSecurityToken',
85      );
86    });
87
88    it('exposes connection type', () => {
89      expect(controller.getConnectionType()).toEqual(AdbConnectionType.MOCK);
90    });
91
92    it('restarts host connection', async () => {
93      await controller.restartConnection();
94      expect(restartSpy).toHaveBeenCalledTimes(1);
95      expect(listener.onConnectionStateChange.calls.mostRecent().args).toEqual([
96        ConnectionState.CONNECTING,
97      ]);
98    });
99
100    it('sets security token', () => {
101      controller.setSecurityToken('12345');
102      expect(securityTokenSpy).toHaveBeenCalledOnceWith('12345');
103    });
104
105    it('requests devices', async () => {
106      await controller.requestDevices();
107      expect(listener.onDevicesChange).toHaveBeenCalledTimes(1);
108      expect(listener.onConnectionStateChange.calls.mostRecent().args).toEqual([
109        ConnectionState.IDLE,
110      ]);
111    });
112
113    it('cancels device requests', async () => {
114      const spy = spyOn(
115        MockAdbHostConnection.prototype,
116        'cancelDeviceRequests',
117      );
118      controller.cancelDeviceRequests();
119      expect(spy).toHaveBeenCalledTimes(1);
120    });
121
122    it('destroys adb session and host on destroy', async () => {
123      runShellCmdSpy.and.returnValue('');
124      await controller.startTrace(mockDevice, [mockUserRequest]);
125      const spies = [
126        spyOn(TracingSession.prototype, 'onDestroy'),
127        hostDestroySpy,
128      ];
129      await controller.onDestroy(mockDevice);
130      spies.forEach((spy) => expect(spy).toHaveBeenCalledTimes(1));
131    });
132  });
133
134  describe('starts traces:', () => {
135    let startSpy: jasmine.Spy<(target: TraceTarget) => Promise<void>>;
136    let userNotifierChecker: UserNotifierChecker;
137
138    beforeEach(async () => {
139      startSpy = spyOn(mockDevice, 'startTrace');
140      runShellCmdSpy.and.returnValue('');
141      userNotifierChecker = new UserNotifierChecker();
142    });
143
144    it('restarts connection if no traces requested', async () => {
145      await controller.startTrace(mockDevice, []);
146      expect(restartSpy).toHaveBeenCalledTimes(1);
147      expect(startSpy).not.toHaveBeenCalled();
148      userNotifierChecker.expectNotified([
149        new ProxyTracingWarnings([
150          'None of the requested targets are available on this device.',
151        ]),
152      ]);
153    });
154
155    it('starts legacy traces', async () => {
156      const target = new TraceTarget('WmLegacyTrace', [], '', '', [
157        new AdbFileIdentifier(
158          '/data/misc/wmtrace/',
159          ['wm_trace.winscope', 'wm_trace.pb'],
160          'window_trace',
161        ),
162      ]);
163      await checkTracingSessionsStarted(
164        [mockUserRequest, mockUserRequest],
165        [target, target],
166      );
167    });
168
169    it('starts perfetto traces', async () => {
170      runShellCmdSpy
171        .withArgs('perfetto --query')
172        .and.returnValue('android.windowmanager');
173      await checkTracingSessionsStarted(
174        [mockUserRequest],
175        [
176          new TraceTarget('PerfettoTrace', [], '', '', [
177            new AdbFileIdentifier(
178              '/data/misc/perfetto-traces/winscope-proxy-trace.perfetto-trace',
179              [],
180              'trace.perfetto-trace',
181            ),
182          ]),
183        ],
184      );
185    });
186
187    async function checkTracingSessionsStarted(
188      requests: UserRequest[],
189      targets: TraceTarget[],
190    ) {
191      startSpy.calls.reset();
192      const stopCurrentSession = spyOn(
193        PerfettoSessionModerator.prototype,
194        'tryStopCurrentPerfettoSession',
195      );
196      const clearPreviousConfigFiles = spyOn(
197        PerfettoSessionModerator.prototype,
198        'clearPreviousConfigFiles',
199      );
200
201      await controller.startTrace(mockDevice, requests);
202
203      expect(stopCurrentSession).toHaveBeenCalledTimes(1);
204      expect(clearPreviousConfigFiles).toHaveBeenCalledTimes(1);
205      expect(runShellCmdSpy.calls.allArgs().slice(1, 3).flat()).toEqual([
206        `su root rm -rf ${WINSCOPE_BACKUP_DIR}`,
207        `su root mkdir ${WINSCOPE_BACKUP_DIR}`,
208      ]);
209      startSpy.calls.allArgs().forEach((args, index) => {
210        expect(args[0].traceName).toEqual(targets[index].traceName);
211        expect(args[0].fileIdentifiers).toEqual(targets[index].fileIdentifiers);
212      });
213      userNotifierChecker.expectNone();
214    }
215  });
216
217  describe('ends traces:', () => {
218    it('ends tracing controller', async () => {
219      const endSpy = spyOn(TracingSession.prototype, 'stop');
220      runShellCmdSpy.and.returnValue('');
221      await controller.startTrace(mockDevice, [
222        mockUserRequest,
223        mockUserRequest,
224      ]);
225      await controller.endTrace(mockDevice);
226      expect(endSpy).toHaveBeenCalledTimes(2);
227      expect(moveSpy).toHaveBeenCalledTimes(2);
228      expect(listener.onProgressUpdate).toHaveBeenCalledTimes(4);
229      expect(listener.onOperationFinished).toHaveBeenCalledTimes(1);
230    });
231  });
232
233  describe('dumps state:', () => {
234    let userNotifierChecker: UserNotifierChecker;
235
236    beforeEach(async () => {
237      runShellCmdSpy.and.returnValue('');
238      userNotifierChecker = new UserNotifierChecker();
239    });
240
241    it('restarts connection if no dumps requested', async () => {
242      await controller.dumpState(mockDevice, []);
243      expect(restartSpy).toHaveBeenCalledTimes(1);
244      expect(runShellCmdSpy).not.toHaveBeenCalled();
245      userNotifierChecker.expectNotified([
246        new ProxyTracingWarnings([
247          'None of the requested targets are available on this device.',
248        ]),
249      ]);
250    });
251
252    it('dumps legacy states', async () => {
253      runShellCmdSpy.calls.reset();
254
255      const expectedCommands = [
256        'su root cmd window tracing frame',
257        'su root cmd window tracing level debug',
258        'su root cmd window tracing size 16000',
259        'su root cmd window tracing start\necho "WM trace (legacy) started."',
260        'su root cmd window tracing frame',
261        'su root cmd window tracing level debug',
262        'su root cmd window tracing size 16000',
263        'su root cmd window tracing start\necho "WM trace (legacy) started."',
264      ];
265
266      await checkDump([mockUserRequest, mockUserRequest], expectedCommands);
267    });
268
269    it('dumps perfetto states', async () => {
270      runShellCmdSpy
271        .withArgs('perfetto --query')
272        .and.returnValue('android.surfaceflinger.layers');
273
274      const req = [
275        {
276          target: UiTraceTarget.SURFACE_FLINGER_DUMP,
277          config: [],
278        },
279      ];
280      const expectedCommands = [
281        `cat << EOF >> /data/misc/perfetto-configs/winscope-proxy-dump.conf
282data_sources: {
283  config {
284    name: "android.surfaceflinger.layers"
285    surfaceflinger_layers_config: {
286      mode: MODE_DUMP
287      trace_flags: TRACE_FLAG_INPUT
288      trace_flags: TRACE_FLAG_COMPOSITION
289      trace_flags: TRACE_FLAG_HWC
290      trace_flags: TRACE_FLAG_BUFFERS
291      trace_flags: TRACE_FLAG_VIRTUAL_DISPLAYS
292    }
293  }
294}
295EOF`,
296        `cat << EOF >> /data/misc/perfetto-configs/winscope-proxy-dump.conf
297buffers: {
298  size_kb: 500000
299  fill_policy: RING_BUFFER
300}
301duration_ms: 1
302EOF
303rm -f /data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace
304perfetto --out /data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace --txt --config /data/misc/perfetto-configs/winscope-proxy-dump.conf
305echo 'Dumped perfetto'`,
306      ];
307      await checkDump(req, expectedCommands);
308    });
309
310    async function checkDump(req: UserRequest[], commands: string[]) {
311      const stopCurrentSession = spyOn(
312        PerfettoSessionModerator.prototype,
313        'tryStopCurrentPerfettoSession',
314      );
315      const clearPreviousConfigFiles = spyOn(
316        PerfettoSessionModerator.prototype,
317        'clearPreviousConfigFiles',
318      );
319
320      await controller.dumpState(mockDevice, req);
321
322      expect(stopCurrentSession).toHaveBeenCalledTimes(1);
323      expect(clearPreviousConfigFiles).toHaveBeenCalledTimes(1);
324
325      const expectedCommands = [
326        'perfetto --query',
327        `su root rm -rf ${WINSCOPE_BACKUP_DIR}`,
328        `su root mkdir ${WINSCOPE_BACKUP_DIR}`,
329      ].concat(commands);
330      runShellCmdSpy.calls.allArgs().forEach((args, index) => {
331        expect(args[0]).toEqual(expectedCommands[index]);
332      });
333      userNotifierChecker.expectNone();
334    }
335  });
336
337  describe('fetches data:', () => {
338    const data = Uint8Array.from([]);
339    const devicePath = 'archive/test_path';
340    const fetchedPath = 'test_path';
341    let findSpy: jasmine.Spy;
342    let pullSpy: jasmine.Spy;
343
344    beforeEach(async () => {
345      findSpy = spyOn(
346        MockAdbDeviceConnection.prototype,
347        'findFiles',
348      ).and.returnValue(Promise.resolve([devicePath, devicePath]));
349      pullSpy = spyOn(
350        MockAdbDeviceConnection.prototype,
351        'pullFile',
352      ).and.returnValue(Promise.resolve(data));
353    });
354
355    it('fetches last tracing session data', async () => {
356      expect(await controller.fetchLastSessionData(mockDevice)).toEqual([
357        new File([data], fetchedPath),
358        new File([data], fetchedPath),
359      ]);
360      expect(listener.onProgressUpdate).toHaveBeenCalledTimes(2);
361      expect(listener.onOperationFinished).toHaveBeenCalledTimes(1);
362    });
363
364    it('does not keep data from last fetch', async () => {
365      await controller.fetchLastSessionData(mockDevice);
366      expect(await controller.fetchLastSessionData(mockDevice)).toEqual([
367        new File([data], fetchedPath),
368        new File([data], fetchedPath),
369      ]);
370    });
371  });
372
373  function resetListener() {
374    listener.onAvailableTracesChange.calls.reset();
375    listener.onDevicesChange.calls.reset();
376    listener.onError.calls.reset();
377    listener.onConnectionStateChange.calls.reset();
378    listener.onProgressUpdate.calls.reset();
379    listener.onOperationFinished.calls.reset();
380  }
381});
382