Remove legacy Winscope

Bug: b/264247967
Test: not needed
Change-Id: I61108714c08e0a4ab2e0c5bf25931740218e68e8
This commit is contained in:
Kean Mariotti
2023-01-03 14:55:41 +00:00
parent 601c771923
commit 969b3358d7
163 changed files with 0 additions and 29153 deletions

View File

@@ -1,5 +0,0 @@
{
"presets": [
"@babel/preset-env"
]
}

View File

@@ -1,35 +0,0 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": [
"plugin:vue/essential",
"google"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"plugins": [
"vue"
],
"rules": {
"require-jsdoc": [
"error",
{
"require": {
"FunctionDeclaration": false,
"MethodDefinition": false,
"ClassDeclaration": false,
"ArrowFunctionExpression": false,
"FunctionExpression": false
}
}
]
}
}

View File

@@ -1,8 +0,0 @@
node_modules/
adb_proxy/venv/
.vscode/
dist/
kotlin_build/
yarn-error.log
kotlin_build/
.eslintcache

View File

@@ -1,5 +0,0 @@
natanieljr@google.com
pablogamito@google.com
keanmariotti@google.com
jjaggi@google.com
roosa@google.com

View File

@@ -1,43 +0,0 @@
# Tool for visualizing window manager traces
## Developing WinScope
When the trace is enabled, Window Manager and Surface Flinger capture and
save current state to a file at each point of interest.
`frameworks/base/core/proto/android/server/windowmanagertrace.proto`
and `frameworks/native/services/surfaceflinger/layerproto/layerstrace.proto`
contain the proto definitions for their internal states.
### Checking out code and setting up environment
* Install [Yarn](https://yarnpkg.com), a JS package manager
* [Download Android source](https://source.android.com/setup/build/downloading)
* Navigate to `development/tools/winscope`
* Run `yarn install`
### Building & testing changes
* Navigate to `development/tools/winscope`
* Run `yarn run dev`
### Update IntDefMapping
* Build `framework-minus-apex-intdefs` module and a preprocessor will
generate the latest IntDefMapping. From the `ANDROID_ROOT` run:
```
. build/envsetup.sh
m framework-minus-apex-intdefs
```
* Copy the generated `intDefMapping.json` files to the `prebuilts` repo.
```
python3 -c 'import sys,json,collections; print(json.dumps(collections.OrderedDict(sorted(collections.ChainMap(*map(lambda x:json.load(open(x)), sys.argv[1:])).items())), indent=2))' $(find out/soong/.intermediates/frameworks/base -iname intDefMapping.json) > ./prebuilts/misc/common/winscope/intDefMapping.json
```
* Upload the changes.
```
cd ./prebuilts/misc/common/winscope
repo start intdef-update
git commit -am "Update intdef mapping" "Test: N/A"
repo upload --cbr .
```
### Building with internal extensions
Internal paths in vendor/ which are not available in AOSP must be replaced by
stub files. See getWaylandSafePath for an example

View File

@@ -1,867 +0,0 @@
#!/usr/bin/python3
# Copyright (C) 2019 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.
#
# This is an ADB proxy for Winscope.
#
# Requirements: python3.5 and ADB installed and in system PATH.
#
# Usage:
# run: python3 winscope_proxy.py
#
import json
import logging
import os
import re
import secrets
import signal
import subprocess
import sys
import threading
import time
from abc import abstractmethod
from enum import Enum
from http import HTTPStatus
from http.server import HTTPServer, BaseHTTPRequestHandler
from tempfile import NamedTemporaryFile
import base64
# CONFIG #
LOG_LEVEL = logging.DEBUG
PORT = 5544
# Keep in sync with WINSCOPE_PROXY_VERSION in Winscope DataAdb.vue
VERSION = '0.8'
WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
WINSCOPE_TOKEN_HEADER = "Winscope-Token"
# Location to save the proxy security token
WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token')
# Winscope traces extensions
WINSCOPE_EXT = ".winscope"
WINSCOPE_EXT_LEGACY = ".pb"
WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY]
# Winscope traces directory
WINSCOPE_DIR = "/data/misc/wmtrace/"
# Max interval between the client keep-alive requests in seconds
KEEP_ALIVE_INTERVAL_S = 5
logging.basicConfig(stream=sys.stderr, level=LOG_LEVEL,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
log = logging.getLogger("ADBProxy")
class File:
def __init__(self, file, filetype) -> None:
self.file = file
self.type = filetype
def get_filepaths(self, device_id):
return [self.file]
def get_filetype(self):
return self.type
class FileMatcher:
def __init__(self, path, matcher, filetype) -> None:
self.path = path
self.matcher = matcher
self.type = filetype
def get_filepaths(self, device_id):
matchingFiles = call_adb(
f"shell su root find {self.path} -name {self.matcher}", device_id)
log.debug("Found file %s", matchingFiles.split('\n')[:-1])
return matchingFiles.split('\n')[:-1]
def get_filetype(self):
return self.type
class WinscopeFileMatcher(FileMatcher):
def __init__(self, path, matcher, filetype) -> None:
self.path = path
self.internal_matchers = list(map(lambda ext: FileMatcher(path, f'{matcher}{ext}', filetype),
WINSCOPE_EXTS))
self.type = filetype
def get_filepaths(self, device_id):
for matcher in self.internal_matchers:
files = matcher.get_filepaths(device_id)
if len(files) > 0:
return files
log.debug("No files found")
return []
class TraceTarget:
"""Defines a single parameter to trace.
Attributes:
file_matchers: the matchers used to identify the paths on the device the trace results are saved to.
trace_start: command to start the trace from adb shell, must not block.
trace_stop: command to stop the trace, should block until the trace is stopped.
"""
def __init__(self, files, trace_start: str, trace_stop: str) -> None:
if type(files) is not list:
files = [files]
self.files = files
self.trace_start = trace_start
self.trace_stop = trace_stop
# Order of files matters as they will be expected in that order and decoded in that order
TRACE_TARGETS = {
"window_trace": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"),
'su root cmd window tracing start\necho "WM trace started."',
'su root cmd window tracing stop >/dev/null 2>&1'
),
"accessibility_trace": TraceTarget(
WinscopeFileMatcher("/data/misc/a11ytrace", "a11y_trace", "accessibility_trace"),
'su root cmd accessibility start-trace\necho "Accessibility trace started."',
'su root cmd accessibility stop-trace >/dev/null 2>&1'
),
"layers_trace": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"),
'su root service call SurfaceFlinger 1025 i32 1\necho "SF trace started."',
'su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1'
),
"screen_recording": TraceTarget(
File(f'/data/local/tmp/screen.mp4', "screen_recording"),
f'screenrecord --bit-rate 8M /data/local/tmp/screen.mp4 >/dev/null 2>&1 &\necho "ScreenRecorder started."',
'pkill -l SIGINT screenrecord >/dev/null 2>&1'
),
"transactions": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"),
'su root service call SurfaceFlinger 1041 i32 1\necho "SF transactions recording started."',
'su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1'
),
"transactions_legacy": TraceTarget(
[
WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"),
FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"),
],
'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."',
'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1'
),
"proto_log": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"),
'su root cmd window logging start\necho "WM logging started."',
'su root cmd window logging stop >/dev/null 2>&1'
),
"ime_trace_clients": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"),
'su root ime tracing start\necho "Clients IME trace started."',
'su root ime tracing stop >/dev/null 2>&1'
),
"ime_trace_service": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"),
'su root ime tracing start\necho "Service IME trace started."',
'su root ime tracing stop >/dev/null 2>&1'
),
"ime_trace_managerservice": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice"),
'su root ime tracing start\necho "ManagerService IME trace started."',
'su root ime tracing stop >/dev/null 2>&1'
),
"wayland_trace": TraceTarget(
WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"),
'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."',
'su root service call Wayland 26 i32 0 >/dev/null'
),
}
class SurfaceFlingerTraceConfig:
"""Handles optional configuration for surfaceflinger traces.
"""
def __init__(self) -> None:
# default config flags CRITICAL | INPUT | SYNC
self.flags = 1 << 0 | 1 << 1 | 1 << 6
def add(self, config: str) -> None:
self.flags |= CONFIG_FLAG[config]
def is_valid(self, config: str) -> bool:
return config in CONFIG_FLAG
def command(self) -> str:
return f'su root service call SurfaceFlinger 1033 i32 {self.flags}'
class SurfaceFlingerTraceSelectedConfig:
"""Handles optional selected configuration for surfaceflinger traces.
"""
def __init__(self) -> None:
# defaults set for all configs
self.selectedConfigs = {
"sfbuffersize": "16000"
}
def add(self, configType, configValue) -> None:
self.selectedConfigs[configType] = configValue
def is_valid(self, configType) -> bool:
return configType in CONFIG_SF_SELECTION
def setBufferSize(self) -> str:
return f'su root service call SurfaceFlinger 1029 i32 {self.selectedConfigs["sfbuffersize"]}'
class WindowManagerTraceSelectedConfig:
"""Handles optional selected configuration for windowmanager traces.
"""
def __init__(self) -> None:
# defaults set for all configs
self.selectedConfigs = {
"wmbuffersize": "16000",
"tracinglevel": "debug",
"tracingtype": "frame",
}
def add(self, configType, configValue) -> None:
self.selectedConfigs[configType] = configValue
def is_valid(self, configType) -> bool:
return configType in CONFIG_WM_SELECTION
def setBufferSize(self) -> str:
return f'su root cmd window tracing size {self.selectedConfigs["wmbuffersize"]}'
def setTracingLevel(self) -> str:
return f'su root cmd window tracing level {self.selectedConfigs["tracinglevel"]}'
def setTracingType(self) -> str:
return f'su root cmd window tracing {self.selectedConfigs["tracingtype"]}'
CONFIG_FLAG = {
"composition": 1 << 2,
"metadata": 1 << 3,
"hwc": 1 << 4,
"tracebuffers": 1 << 5
}
#Keep up to date with options in DataAdb.vue
CONFIG_SF_SELECTION = [
"sfbuffersize",
]
#Keep up to date with options in DataAdb.vue
CONFIG_WM_SELECTION = [
"wmbuffersize",
"tracingtype",
"tracinglevel",
]
class DumpTarget:
"""Defines a single parameter to trace.
Attributes:
file: the path on the device the dump results are saved to.
dump_command: command to dump state to file.
"""
def __init__(self, files, dump_command: str) -> None:
if type(files) is not list:
files = [files]
self.files = files
self.dump_command = dump_command
DUMP_TARGETS = {
"window_dump": DumpTarget(
File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"),
f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}'
),
"layers_dump": DumpTarget(
File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"),
f'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}'
)
}
# END OF CONFIG #
def get_token() -> str:
"""Returns saved proxy security token or creates new one"""
try:
with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file:
token = token_file.readline()
log.debug("Loaded token {} from {}".format(
token, WINSCOPE_TOKEN_LOCATION))
return token
except IOError:
token = secrets.token_hex(32)
os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True)
try:
with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file:
log.debug("Created and saved token {} to {}".format(
token, WINSCOPE_TOKEN_LOCATION))
token_file.write(token)
os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600)
except IOError:
log.error("Unable to save persistent token {} to {}".format(
token, WINSCOPE_TOKEN_LOCATION))
return token
secret_token = get_token()
class RequestType(Enum):
GET = 1
POST = 2
HEAD = 3
def add_standard_headers(server):
server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
server.send_header('Access-Control-Allow-Origin', '*')
server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
server.send_header('Access-Control-Allow-Headers',
WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length')
server.send_header('Access-Control-Expose-Headers',
'Winscope-Proxy-Version')
server.send_header(WINSCOPE_VERSION_HEADER, VERSION)
server.end_headers()
class RequestEndpoint:
"""Request endpoint to use with the RequestRouter."""
@abstractmethod
def process(self, server, path):
pass
class AdbError(Exception):
"""Unsuccessful ADB operation"""
pass
class BadRequest(Exception):
"""Invalid client request"""
pass
class RequestRouter:
"""Handles HTTP request authentication and routing"""
def __init__(self, handler):
self.request = handler
self.endpoints = {}
def register_endpoint(self, method: RequestType, name: str, endpoint: RequestEndpoint):
self.endpoints[(method, name)] = endpoint
def __bad_request(self, error: str):
log.warning("Bad request: " + error)
self.request.respond(HTTPStatus.BAD_REQUEST, b"Bad request!\nThis is Winscope ADB proxy.\n\n"
+ error.encode("utf-8"), 'text/txt')
def __internal_error(self, error: str):
log.error("Internal error: " + error)
self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR,
error.encode("utf-8"), 'text/txt')
def __bad_token(self):
log.info("Bad token")
self.request.respond(HTTPStatus.FORBIDDEN, b"Bad Winscope authorisation token!\nThis is Winscope ADB proxy.\n",
'text/txt')
def process(self, method: RequestType):
token = self.request.headers[WINSCOPE_TOKEN_HEADER]
if not token or token != secret_token:
return self.__bad_token()
path = self.request.path.strip('/').split('/')
if path and len(path) > 0:
endpoint_name = path[0]
try:
return self.endpoints[(method, endpoint_name)].process(self.request, path[1:])
except KeyError:
return self.__bad_request("Unknown endpoint /{}/".format(endpoint_name))
except AdbError as ex:
return self.__internal_error(str(ex))
except BadRequest as ex:
return self.__bad_request(str(ex))
except Exception as ex:
return self.__internal_error(repr(ex))
self.__bad_request("No endpoint specified")
def call_adb(params: str, device: str = None, stdin: bytes = None):
command = ['adb'] + (['-s', device] if device else []) + params.split(' ')
try:
log.debug("Call: " + ' '.join(command))
return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8')
except OSError as ex:
log.debug('Error executing adb command: {}\n{}'.format(
' '.join(command), repr(ex)))
raise AdbError('Error executing adb command: {}\n{}'.format(
' '.join(command), repr(ex)))
except subprocess.CalledProcessError as ex:
log.debug('Error executing adb command: {}\n{}'.format(
' '.join(command), ex.output.decode("utf-8")))
raise AdbError('Error executing adb command: adb {}\n{}'.format(
params, ex.output.decode("utf-8")))
def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None):
try:
process = subprocess.Popen(['adb'] + (['-s', device] if device else []) + params.split(' '), stdout=outfile,
stderr=subprocess.PIPE)
_, err = process.communicate(stdin)
outfile.seek(0)
if process.returncode != 0:
log.debug('Error executing adb command: adb {}\n'.format(params) + err.decode(
'utf-8') + '\n' + outfile.read().decode('utf-8'))
raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode(
'utf-8') + '\n' + outfile.read().decode('utf-8'))
except OSError as ex:
log.debug('Error executing adb command: adb {}\n{}'.format(
params, repr(ex)))
raise AdbError(
'Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
class CheckWaylandServiceEndpoint(RequestEndpoint):
_listDevicesEndpoint = None
def __init__(self, listDevicesEndpoint):
self._listDevicesEndpoint = listDevicesEndpoint
def process(self, server, path):
self._listDevicesEndpoint.process(server, path)
foundDevices = self._listDevicesEndpoint._foundDevices
if len(foundDevices) > 1:
res = 'false'
else:
raw_res = call_adb('shell service check Wayland')
res = 'false' if 'not found' in raw_res else 'true'
server.respond(HTTPStatus.OK, res.encode("utf-8"), "text/json")
class ListDevicesEndpoint(RequestEndpoint):
ADB_INFO_RE = re.compile("^([A-Za-z0-9.:\\-]+)\\s+(\\w+)(.*model:(\\w+))?")
_foundDevices = None
def process(self, server, path):
lines = list(filter(None, call_adb('devices -l').split('\n')))
devices = {m.group(1): {
'authorised': str(m.group(2)) != 'unauthorized',
'model': m.group(4).replace('_', ' ') if m.group(4) else ''
} for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]] if m}
self._foundDevices = devices
j = json.dumps(devices)
log.debug("Detected devices: " + j)
server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
class DeviceRequestEndpoint(RequestEndpoint):
def process(self, server, path):
if len(path) > 0 and re.fullmatch("[A-Za-z0-9.:\\-]+", path[0]):
self.process_with_device(server, path[1:], path[0])
else:
raise BadRequest("Device id not specified")
@abstractmethod
def process_with_device(self, server, path, device_id):
pass
def get_request(self, server) -> str:
try:
length = int(server.headers["Content-Length"])
except KeyError as err:
raise BadRequest("Missing Content-Length header\n" + str(err))
except ValueError as err:
raise BadRequest("Content length unreadable\n" + str(err))
return json.loads(server.rfile.read(length).decode("utf-8"))
class FetchFilesEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
if len(path) != 1:
raise BadRequest("File not specified")
if path[0] in TRACE_TARGETS:
files = TRACE_TARGETS[path[0]].files
elif path[0] in DUMP_TARGETS:
files = DUMP_TARGETS[path[0]].files
else:
raise BadRequest("Unknown file specified")
file_buffers = dict()
for f in files:
file_type = f.get_filetype()
file_paths = f.get_filepaths(device_id)
for file_path in file_paths:
with NamedTemporaryFile() as tmp:
log.debug(
f"Fetching file {file_path} from device to {tmp.name}")
call_adb_outfile('exec-out su root cat ' +
file_path, tmp, device_id)
log.debug(f"Deleting file {file_path} from device")
call_adb('shell su root rm ' + file_path, device_id)
log.debug(f"Uploading file {tmp.name}")
if file_type not in file_buffers:
file_buffers[file_type] = []
buf = base64.encodebytes(tmp.read()).decode("utf-8")
file_buffers[file_type].append(buf)
if (len(file_buffers) == 0):
log.error("Proxy didn't find any file to fetch")
# server.send_header('X-Content-Type-Options', 'nosniff')
# add_standard_headers(server)
j = json.dumps(file_buffers)
server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
def check_root(device_id):
log.debug("Checking root access on {}".format(device_id))
return int(call_adb('shell su root id -u', device_id)) == 0
TRACE_THREADS = {}
class TraceThread(threading.Thread):
def __init__(self, device_id, command):
self._keep_alive_timer = None
self.trace_command = command
self._device_id = device_id
self.out = None,
self.err = None,
self._success = False
try:
shell = ['adb', '-s', self._device_id, 'shell']
log.debug("Starting trace shell {}".format(' '.join(shell)))
self.process = subprocess.Popen(shell, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True)
except OSError as ex:
raise AdbError(
'Error executing adb command: adb shell\n{}'.format(repr(ex)))
super().__init__()
def timeout(self):
if self.is_alive():
log.warning(
"Keep-alive timeout for trace on {}".format(self._device_id))
self.end_trace()
if self._device_id in TRACE_THREADS:
TRACE_THREADS.pop(self._device_id)
def reset_timer(self):
log.debug(
"Resetting keep-alive clock for trace on {}".format(self._device_id))
if self._keep_alive_timer:
self._keep_alive_timer.cancel()
self._keep_alive_timer = threading.Timer(
KEEP_ALIVE_INTERVAL_S, self.timeout)
self._keep_alive_timer.start()
def end_trace(self):
if self._keep_alive_timer:
self._keep_alive_timer.cancel()
log.debug("Sending SIGINT to the trace process on {}".format(
self._device_id))
self.process.send_signal(signal.SIGINT)
try:
log.debug("Waiting for trace shell to exit for {}".format(
self._device_id))
self.process.wait(timeout=5)
except TimeoutError:
log.debug(
"TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id))
self.process.kill()
self.join()
def run(self):
log.debug("Trace started on {}".format(self._device_id))
self.reset_timer()
self.out, self.err = self.process.communicate(self.trace_command)
log.debug("Trace ended on {}, waiting for cleanup".format(self._device_id))
time.sleep(0.2)
for i in range(50):
if call_adb("shell su root cat /data/local/tmp/winscope_status", device=self._device_id) == 'TRACE_OK\n':
call_adb(
"shell su root rm /data/local/tmp/winscope_status", device=self._device_id)
log.debug("Trace finished successfully on {}".format(
self._device_id))
self._success = True
break
log.debug("Still waiting for cleanup on {}".format(self._device_id))
time.sleep(0.1)
def success(self):
return self._success
class StartTrace(DeviceRequestEndpoint):
TRACE_COMMAND = """
set -e
echo "Starting trace..."
echo "TRACE_START" > /data/local/tmp/winscope_status
# Do not print anything to stdout/stderr in the handler
function stop_trace() {{
trap - EXIT HUP INT
{}
echo "TRACE_OK" > /data/local/tmp/winscope_status
}}
trap stop_trace EXIT HUP INT
echo "Signal handler registered."
{}
# ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground,
# as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval.
while true; do sleep 0.1; done
"""
def process_with_device(self, server, path, device_id):
try:
requested_types = self.get_request(server)
requested_traces = [TRACE_TARGETS[t] for t in requested_types]
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
log.warning("Trace already in progress for {}", device_id)
server.respond(HTTPStatus.OK, b'', "text/plain")
if not check_root(device_id):
raise AdbError(
"Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'".format(
device_id))
command = StartTrace.TRACE_COMMAND.format(
'\n'.join([t.trace_stop for t in requested_traces]),
'\n'.join([t.trace_start for t in requested_traces]))
log.debug("Trace requested for {} with targets {}".format(
device_id, ','.join(requested_types)))
TRACE_THREADS[device_id] = TraceThread(
device_id, command.encode('utf-8'))
TRACE_THREADS[device_id].start()
server.respond(HTTPStatus.OK, b'', "text/plain")
class EndTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
if device_id not in TRACE_THREADS:
raise BadRequest("No trace in progress for {}".format(device_id))
if TRACE_THREADS[device_id].is_alive():
TRACE_THREADS[device_id].end_trace()
success = TRACE_THREADS[device_id].success()
out = TRACE_THREADS[device_id].out + \
b"\n" + TRACE_THREADS[device_id].err
command = TRACE_THREADS[device_id].trace_command
TRACE_THREADS.pop(device_id)
if success:
server.respond(HTTPStatus.OK, out, "text/plain")
else:
raise AdbError(
"Error tracing the device\n### Output ###\n" + out.decode(
"utf-8") + "\n### Command: adb -s {} shell ###\n### Input ###\n".format(device_id) + command.decode(
"utf-8"))
def execute_command(server, device_id, shell, configType, configValue):
process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE, start_new_session=True)
log.debug(f"Changing trace config on device {device_id} {configType}:{configValue}")
out, err = process.communicate(configValue.encode('utf-8'))
if process.returncode != 0:
raise AdbError(
f"Error executing command:\n {configValue}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}")
log.debug(f"Changing trace config finished on device {device_id}")
server.respond(HTTPStatus.OK, b'', "text/plain")
class ConfigTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
try:
requested_configs = self.get_request(server)
config = SurfaceFlingerTraceConfig()
for requested_config in requested_configs:
if not config.is_valid(requested_config):
raise BadRequest(
f"Unsupported config {requested_config}\n")
config.add(requested_config)
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
BadRequest(f"Trace in progress for {device_id}")
if not check_root(device_id):
raise AdbError(
f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
command = config.command()
shell = ['adb', '-s', device_id, 'shell']
log.debug(f"Starting shell {' '.join(shell)}")
execute_command(server, device_id, shell, "sf buffer size", command)
def add_selected_request_to_config(self, server, device_id, config):
try:
requested_configs = self.get_request(server)
for requested_config in requested_configs:
if config.is_valid(requested_config):
config.add(requested_config, requested_configs[requested_config])
else:
raise BadRequest(
f"Unsupported config {requested_config}\n")
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
BadRequest(f"Trace in progress for {device_id}")
if not check_root(device_id):
raise AdbError(
f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
return config
class SurfaceFlingerSelectedConfigTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
config = SurfaceFlingerTraceSelectedConfig()
config = add_selected_request_to_config(self, server, device_id, config)
setBufferSize = config.setBufferSize()
shell = ['adb', '-s', device_id, 'shell']
log.debug(f"Starting shell {' '.join(shell)}")
execute_command(server, device_id, shell, "sf buffer size", setBufferSize)
class WindowManagerSelectedConfigTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
config = WindowManagerTraceSelectedConfig()
config = add_selected_request_to_config(self, server, device_id, config)
setBufferSize = config.setBufferSize()
setTracingType = config.setTracingType()
setTracingLevel = config.setTracingLevel()
shell = ['adb', '-s', device_id, 'shell']
log.debug(f"Starting shell {' '.join(shell)}")
execute_command(server, device_id, shell, "wm buffer size", setBufferSize)
execute_command(server, device_id, shell, "tracing type", setTracingType)
execute_command(server, device_id, shell, "tracing level", setTracingLevel)
class StatusEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
if device_id not in TRACE_THREADS:
raise BadRequest("No trace in progress for {}".format(device_id))
TRACE_THREADS[device_id].reset_timer()
server.respond(HTTPStatus.OK, str(
TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain")
class DumpEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
try:
requested_types = self.get_request(server)
requested_traces = [DUMP_TARGETS[t] for t in requested_types]
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
BadRequest("Trace in progress for {}".format(device_id))
if not check_root(device_id):
raise AdbError(
"Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'"
.format(device_id))
command = '\n'.join(t.dump_command for t in requested_traces)
shell = ['adb', '-s', device_id, 'shell']
log.debug("Starting dump shell {}".format(' '.join(shell)))
process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE, start_new_session=True)
log.debug("Starting dump on device {}".format(device_id))
out, err = process.communicate(command.encode('utf-8'))
if process.returncode != 0:
raise AdbError("Error executing command:\n" + command + "\n\n### OUTPUT ###" + out.decode('utf-8') + "\n"
+ err.decode('utf-8'))
log.debug("Dump finished on device {}".format(device_id))
server.respond(HTTPStatus.OK, b'', "text/plain")
class ADBWinscopeProxy(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
self.router = RequestRouter(self)
listDevicesEndpoint = ListDevicesEndpoint()
self.router.register_endpoint(
RequestType.GET, "devices", listDevicesEndpoint)
self.router.register_endpoint(
RequestType.GET, "status", StatusEndpoint())
self.router.register_endpoint(
RequestType.GET, "fetch", FetchFilesEndpoint())
self.router.register_endpoint(RequestType.POST, "start", StartTrace())
self.router.register_endpoint(RequestType.POST, "end", EndTrace())
self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint())
self.router.register_endpoint(
RequestType.POST, "configtrace", ConfigTrace())
self.router.register_endpoint(
RequestType.POST, "selectedsfconfigtrace", SurfaceFlingerSelectedConfigTrace())
self.router.register_endpoint(
RequestType.POST, "selectedwmconfigtrace", WindowManagerSelectedConfigTrace())
self.router.register_endpoint(
RequestType.GET, "checkwayland", CheckWaylandServiceEndpoint(listDevicesEndpoint))
super().__init__(request, client_address, server)
def respond(self, code: int, data: bytes, mime: str) -> None:
self.send_response(code)
self.send_header('Content-type', mime)
add_standard_headers(self)
self.wfile.write(data)
def do_GET(self):
self.router.process(RequestType.GET)
def do_POST(self):
self.router.process(RequestType.POST)
def do_OPTIONS(self):
self.send_response(HTTPStatus.OK)
self.send_header('Allow', 'GET,POST')
add_standard_headers(self)
self.end_headers()
self.wfile.write(b'GET,POST')
def log_request(self, code='-', size='-'):
log.info('{} {} {}'.format(self.requestline, str(code), str(size)))
if __name__ == '__main__':
print("Winscope ADB Connect proxy version: " + VERSION)
print('Winscope token: ' + secret_token)
httpd = HTTPServer(('localhost', PORT), ADBWinscopeProxy)
try:
httpd.serve_forever()
except KeyboardInterrupt:
log.info("Shutting down")

View File

@@ -1,19 +0,0 @@
/*
* Copyright 2020, 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.
*/
module.exports = {
NODE_ENV: 'development'
};

View File

@@ -1,19 +0,0 @@
/*
* Copyright 2020, 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.
*/
module.exports = {
NODE_ENV: 'production'
};

View File

@@ -1,81 +0,0 @@
import Vue from 'vue'
import { ItemProps, SlotProps } from './props'
const Wrapper = {
created() {
this.shapeKey = this.horizontal ? 'offsetWidth' : 'offsetHeight'
},
mounted() {
if (typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver(() => {
this.dispatchSizeChange()
})
this.resizeObserver.observe(this.$el)
}
},
// since component will be reused, so dispatch when updated
updated() {
this.dispatchSizeChange()
},
beforeDestroy() {
if (this.resizeObserver) {
this.resizeObserver.disconnect()
this.resizeObserver = null
}
},
methods: {
getCurrentSize() {
return this.$el ? this.$el[this.shapeKey] : 0
},
// tell parent current size identify by unique key
dispatchSizeChange() {
this.$parent.$emit(this.event, this.uniqueKey, this.getCurrentSize(), this.hasInitial)
}
}
}
// wrapping for item
export const Item = Vue.component('virtual-list-item', {
mixins: [Wrapper],
props: ItemProps,
render(h) {
const { tag, component, extraProps = {}, index, scopedSlots = {}, uniqueKey } = this
extraProps.source = this.source
extraProps.index = index
return h(tag, {
key: uniqueKey,
attrs: {
role: 'item'
}
}, [h(component, {
props: extraProps,
scopedSlots: scopedSlots
})])
}
})
// wrapping for slot
export const Slot = Vue.component('virtual-list-slot', {
mixins: [Wrapper],
props: SlotProps,
render(h) {
const { tag, uniqueKey } = this
return h(tag, {
key: uniqueKey,
attrs: {
role: uniqueKey
}
}, this.$slots.default)
}
})

View File

@@ -1,3 +0,0 @@
# vue-virtual-scroll-list
The original library can be found on GitHub at [https://github.com/tangbc/vue-virtual-scroll-list](https://github.com/tangbc/vue-virtual-scroll-list).

View File

@@ -1,354 +0,0 @@
import Vue from 'vue'
import Virtual from './virtual'
import { Item, Slot } from './Item'
import { VirtualProps } from './props'
const EVENT_TYPE = {
ITEM: 'item_resize',
SLOT: 'slot_resize'
}
const SLOT_TYPE = {
HEADER: 'header', // string value also use for aria role attribute
FOOTER: 'footer'
}
const VirtualList = Vue.component('virtual-list', {
props: VirtualProps,
data() {
return {
range: null
}
},
watch: {
'dataSources.length'() {
this.virtual.updateParam('uniqueIds', this.getUniqueIdFromDataSources())
this.virtual.handleDataSourcesChange()
},
start(newValue) {
this.scrollToIndex(newValue)
},
offset(newValue) {
this.scrollToOffset(newValue)
}
},
created() {
this.isHorizontal = this.direction === 'horizontal'
this.directionKey = this.isHorizontal ? 'scrollLeft' : 'scrollTop'
this.installVirtual()
// listen item size change
this.$on(EVENT_TYPE.ITEM, this.onItemResized)
// listen slot size change
if (this.$slots.header || this.$slots.footer) {
this.$on(EVENT_TYPE.SLOT, this.onSlotResized)
}
},
// set back offset when awake from keep-alive
activated() {
this.scrollToOffset(this.virtual.offset)
},
mounted() {
// set position
if (this.start) {
this.scrollToIndex(this.start)
} else if (this.offset) {
this.scrollToOffset(this.offset)
}
// in page mode we bind scroll event to document
if (this.pageMode) {
this.updatePageModeFront()
document.addEventListener('scroll', this.onScroll, {
passive: false
})
}
},
beforeDestroy() {
this.virtual.destroy()
if (this.pageMode) {
document.removeEventListener('scroll', this.onScroll)
}
},
methods: {
// get item size by id
getSize(id) {
return this.virtual.sizes.get(id)
},
// get the total number of stored (rendered) items
getSizes() {
return this.virtual.sizes.size
},
// return current scroll offset
getOffset() {
if (this.pageMode) {
return document.documentElement[this.directionKey] || document.body[this.directionKey]
} else {
const { root } = this.$refs
return root ? Math.ceil(root[this.directionKey]) : 0
}
},
// return client viewport size
getClientSize() {
const key = this.isHorizontal ? 'clientWidth' : 'clientHeight'
if (this.pageMode) {
return document.documentElement[key] || document.body[key]
} else {
const { root } = this.$refs
return root ? Math.ceil(root[key]) : 0
}
},
// return all scroll size
getScrollSize() {
const key = this.isHorizontal ? 'scrollWidth' : 'scrollHeight'
if (this.pageMode) {
return document.documentElement[key] || document.body[key]
} else {
const { root } = this.$refs
return root ? Math.ceil(root[key]) : 0
}
},
// set current scroll position to a expectant offset
scrollToOffset(offset) {
if (this.pageMode) {
document.body[this.directionKey] = offset
document.documentElement[this.directionKey] = offset
} else {
const { root } = this.$refs
if (root) {
root[this.directionKey] = offset
}
}
},
// set current scroll position to a expectant index
scrollToIndex(index) {
// scroll to bottom
if (index >= this.dataSources.length - 1) {
this.scrollToBottom()
} else {
const offset = this.virtual.getOffset(index)
this.scrollToOffset(offset)
}
},
// set current scroll position to bottom
scrollToBottom() {
const { shepherd } = this.$refs
if (shepherd) {
const offset = shepherd[this.isHorizontal ? 'offsetLeft' : 'offsetTop']
this.scrollToOffset(offset)
// check if it's really scrolled to the bottom
// maybe list doesn't render and calculate to last range
// so we need retry in next event loop until it really at bottom
setTimeout(() => {
if (this.getOffset() + this.getClientSize() < this.getScrollSize()) {
this.scrollToBottom()
}
}, 3)
}
},
// when using page mode we need update slot header size manually
// taking root offset relative to the browser as slot header size
updatePageModeFront() {
const { root } = this.$refs
if (root) {
const rect = root.getBoundingClientRect()
const { defaultView } = root.ownerDocument
const offsetFront = this.isHorizontal ? (rect.left + defaultView.pageXOffset) : (rect.top + defaultView.pageYOffset)
this.virtual.updateParam('slotHeaderSize', offsetFront)
}
},
// reset all state back to initial
reset() {
this.virtual.destroy()
this.scrollToOffset(0)
this.installVirtual()
},
// ----------- public method end -----------
installVirtual() {
this.virtual = new Virtual({
slotHeaderSize: 0,
slotFooterSize: 0,
keeps: this.keeps,
estimateSize: this.estimateSize,
buffer: Math.round(this.keeps / 3), // recommend for a third of keeps
uniqueIds: this.getUniqueIdFromDataSources()
}, this.onRangeChanged)
// sync initial range
this.range = this.virtual.getRange()
},
getUniqueIdFromDataSources() {
const { dataKey } = this
return this.dataSources.map((dataSource) => typeof dataKey === 'function' ? dataKey(dataSource) : dataSource[dataKey])
},
// event called when each item mounted or size changed
onItemResized(id, size) {
this.virtual.saveSize(id, size)
this.$emit('resized', id, size)
},
// event called when slot mounted or size changed
onSlotResized(type, size, hasInit) {
if (type === SLOT_TYPE.HEADER) {
this.virtual.updateParam('slotHeaderSize', size)
} else if (type === SLOT_TYPE.FOOTER) {
this.virtual.updateParam('slotFooterSize', size)
}
if (hasInit) {
this.virtual.handleSlotSizeChange()
}
},
// here is the re-rendering entry
onRangeChanged(range) {
this.range = range
},
onScroll(evt) {
const offset = this.getOffset()
const clientSize = this.getClientSize()
const scrollSize = this.getScrollSize()
// iOS scroll-spring-back behavior will make direction mistake
if (offset < 0 || (offset + clientSize > scrollSize + 1) || !scrollSize) {
return
}
this.virtual.handleScroll(offset)
this.emitEvent(offset, clientSize, scrollSize, evt)
},
// emit event in special position
emitEvent(offset, clientSize, scrollSize, evt) {
this.$emit('scroll', evt, this.virtual.getRange())
if (this.virtual.isFront() && !!this.dataSources.length && (offset - this.topThreshold <= 0)) {
this.$emit('totop')
} else if (this.virtual.isBehind() && (offset + clientSize + this.bottomThreshold >= scrollSize)) {
this.$emit('tobottom')
}
},
// get the real render slots based on range data
// in-place patch strategy will try to reuse components as possible
// so those components that are reused will not trigger lifecycle mounted
getRenderSlots(h) {
const slots = []
const { start, end } = this.range
const { dataSources, dataKey, itemClass, itemTag, itemStyle, isHorizontal, extraProps, dataComponent, itemScopedSlots } = this
for (let index = start; index <= end; index++) {
const dataSource = dataSources[index]
if (dataSource) {
const uniqueKey = typeof dataKey === 'function' ? dataKey(dataSource) : dataSource[dataKey]
if (typeof uniqueKey === 'string' || typeof uniqueKey === 'number') {
slots.push(h(Item, {
props: {
index,
tag: itemTag,
event: EVENT_TYPE.ITEM,
horizontal: isHorizontal,
uniqueKey: uniqueKey,
source: dataSource,
extraProps: extraProps,
component: dataComponent,
scopedSlots: itemScopedSlots
},
style: itemStyle,
class: `${itemClass}${this.itemClassAdd ? ' ' + this.itemClassAdd(index) : ''}`
}))
} else {
console.warn(`Cannot get the data-key '${dataKey}' from data-sources.`)
}
} else {
console.warn(`Cannot get the index '${index}' from data-sources.`)
}
}
return slots
}
},
// render function, a closer-to-the-compiler alternative to templates
// https://vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth
render(h) {
const { header, footer } = this.$slots
const { padFront, padBehind } = this.range
const { isHorizontal, pageMode, rootTag, wrapTag, wrapClass, wrapStyle, headerTag, headerClass, headerStyle, footerTag, footerClass, footerStyle } = this
const paddingStyle = { padding: isHorizontal ? `0px ${padBehind}px 0px ${padFront}px` : `${padFront}px 0px ${padBehind}px` }
const wrapperStyle = wrapStyle ? Object.assign({}, wrapStyle, paddingStyle) : paddingStyle
return h(rootTag, {
ref: 'root',
on: {
'&scroll': !pageMode && this.onScroll
}
}, [
// header slot
header ? h(Slot, {
class: headerClass,
style: headerStyle,
props: {
tag: headerTag,
event: EVENT_TYPE.SLOT,
uniqueKey: SLOT_TYPE.HEADER
}
}, header) : null,
// main list
h(wrapTag, {
class: wrapClass,
attrs: {
role: 'group'
},
style: wrapperStyle
}, this.getRenderSlots(h)),
// footer slot
footer ? h(Slot, {
class: footerClass,
style: footerStyle,
props: {
tag: footerTag,
event: EVENT_TYPE.SLOT,
uniqueKey: SLOT_TYPE.FOOTER
}
}, footer) : null,
// an empty element use to scroll to bottom
h('div', {
ref: 'shepherd',
style: {
width: isHorizontal ? '0px' : '100%',
height: isHorizontal ? '100%' : '0px'
}
})
])
}
})
export default VirtualList

View File

@@ -1,150 +0,0 @@
export const VirtualProps = {
dataKey: {
type: [String, Function],
required: true
},
dataSources: {
type: Array,
required: true
},
dataComponent: {
type: [Object, Function],
required: true
},
keeps: {
type: Number,
default: 30
},
extraProps: {
type: Object
},
estimateSize: {
type: Number,
default: 50
},
direction: {
type: String,
default: 'vertical' // the other value is horizontal
},
start: {
type: Number,
default: 0
},
offset: {
type: Number,
default: 0
},
topThreshold: {
type: Number,
default: 0
},
bottomThreshold: {
type: Number,
default: 0
},
pageMode: {
type: Boolean,
default: false
},
rootTag: {
type: String,
default: 'div'
},
wrapTag: {
type: String,
default: 'div'
},
wrapClass: {
type: String,
default: ''
},
wrapStyle: {
type: Object
},
itemTag: {
type: String,
default: 'div'
},
itemClass: {
type: String,
default: ''
},
itemClassAdd: {
type: Function
},
itemStyle: {
type: Object
},
headerTag: {
type: String,
default: 'div'
},
headerClass: {
type: String,
default: ''
},
headerStyle: {
type: Object
},
footerTag: {
type: String,
default: 'div'
},
footerClass: {
type: String,
default: ''
},
footerStyle: {
type: Object
},
itemScopedSlots: {
type: Object
}
}
export const ItemProps = {
index: {
type: Number
},
event: {
type: String
},
tag: {
type: String
},
horizontal: {
type: Boolean
},
source: {
type: Object
},
component: {
type: [Object, Function]
},
uniqueKey: {
type: [String, Number]
},
extraProps: {
type: Object
},
scopedSlots: {
type: Object
}
}
export const SlotProps = {
event: {
type: String
},
uniqueKey: {
type: String
},
tag: {
type: String
},
horizontal: {
type: Boolean
}
}

View File

@@ -1,305 +0,0 @@
const DIRECTION_TYPE = {
FRONT: 'FRONT', // scroll up or left
BEHIND: 'BEHIND' // scroll down or right
}
const CALC_TYPE = {
INIT: 'INIT',
FIXED: 'FIXED',
DYNAMIC: 'DYNAMIC'
}
const LEADING_BUFFER = 2
export default class Virtual {
constructor(param, callUpdate) {
this.init(param, callUpdate)
}
init(param, callUpdate) {
// param data
this.param = param
this.callUpdate = callUpdate
// size data
this.sizes = new Map()
this.firstRangeTotalSize = 0
this.firstRangeAverageSize = 0
this.lastCalcIndex = 0
this.fixedSizeValue = 0
this.calcType = CALC_TYPE.INIT
// scroll data
this.offset = 0
this.direction = ''
// range data
this.range = Object.create(null)
if (param) {
this.checkRange(0, param.keeps - 1)
}
// benchmark test data
// this.__bsearchCalls = 0
// this.__getIndexOffsetCalls = 0
}
destroy() {
this.init(null, null)
}
// return current render range
getRange() {
const range = Object.create(null)
range.start = this.range.start
range.end = this.range.end
range.padFront = this.range.padFront
range.padBehind = this.range.padBehind
return range
}
isBehind() {
return this.direction === DIRECTION_TYPE.BEHIND
}
isFront() {
return this.direction === DIRECTION_TYPE.FRONT
}
// return start index offset
getOffset(start) {
return (start < 1 ? 0 : this.getIndexOffset(start)) + this.param.slotHeaderSize
}
updateParam(key, value) {
if (this.param && (key in this.param)) {
// if uniqueIds change, find out deleted id and remove from size map
if (key === 'uniqueIds') {
this.sizes.forEach((v, key) => {
if (!value.includes(key)) {
this.sizes.delete(key)
}
})
}
this.param[key] = value
}
}
// save each size map by id
saveSize(id, size) {
this.sizes.set(id, size)
// we assume size type is fixed at the beginning and remember first size value
// if there is no size value different from this at next coming saving
// we think it's a fixed size list, otherwise is dynamic size list
if (this.calcType === CALC_TYPE.INIT) {
this.fixedSizeValue = size
this.calcType = CALC_TYPE.FIXED
} else if (this.calcType === CALC_TYPE.FIXED && this.fixedSizeValue !== size) {
this.calcType = CALC_TYPE.DYNAMIC
// it's no use at all
delete this.fixedSizeValue
}
// calculate the average size only in the first range
if (this.calcType !== CALC_TYPE.FIXED && typeof this.firstRangeTotalSize !== 'undefined') {
if (this.sizes.size < Math.min(this.param.keeps, this.param.uniqueIds.length)) {
this.firstRangeTotalSize = this.firstRangeTotalSize + size
this.firstRangeAverageSize = Math.round(this.firstRangeTotalSize / this.sizes.size)
} else {
// it's done using
delete this.firstRangeTotalSize
}
}
}
// in some special situation (e.g. length change) we need to update in a row
// try going to render next range by a leading buffer according to current direction
handleDataSourcesChange() {
let start = this.range.start
if (this.isFront()) {
start = start - LEADING_BUFFER
} else if (this.isBehind()) {
start = start + LEADING_BUFFER
}
start = Math.max(start, 0)
this.updateRange(this.range.start, this.getEndByStart(start))
}
// when slot size change, we also need force update
handleSlotSizeChange() {
this.handleDataSourcesChange()
}
// calculating range on scroll
handleScroll(offset) {
this.direction = offset < this.offset ? DIRECTION_TYPE.FRONT : DIRECTION_TYPE.BEHIND
this.offset = offset
if (this.direction === DIRECTION_TYPE.FRONT) {
this.handleFront()
} else if (this.direction === DIRECTION_TYPE.BEHIND) {
this.handleBehind()
}
}
// ----------- public method end -----------
handleFront() {
const overs = this.getScrollOvers()
// should not change range if start doesn't exceed overs
if (overs > this.range.start) {
return
}
// move up start by a buffer length, and make sure its safety
const start = Math.max(overs - this.param.buffer, 0)
this.checkRange(start, this.getEndByStart(start))
}
handleBehind() {
const overs = this.getScrollOvers()
// range should not change if scroll overs within buffer
if (overs < this.range.start + this.param.buffer) {
return
}
this.checkRange(overs, this.getEndByStart(overs))
}
// return the pass overs according to current scroll offset
getScrollOvers() {
// if slot header exist, we need subtract its size
const offset = this.offset - this.param.slotHeaderSize
if (offset <= 0) {
return 0
}
// if is fixed type, that can be easily
if (this.isFixedType()) {
return Math.floor(offset / this.fixedSizeValue)
}
let low = 0
let middle = 0
let middleOffset = 0
let high = this.param.uniqueIds.length
while (low <= high) {
// this.__bsearchCalls++
middle = low + Math.floor((high - low) / 2)
middleOffset = this.getIndexOffset(middle)
if (middleOffset === offset) {
return middle
} else if (middleOffset < offset) {
low = middle + 1
} else if (middleOffset > offset) {
high = middle - 1
}
}
return low > 0 ? --low : 0
}
// return a scroll offset from given index, can efficiency be improved more here?
// although the call frequency is very high, its only a superposition of numbers
getIndexOffset(givenIndex) {
if (!givenIndex) {
return 0
}
let offset = 0
let indexSize = 0
for (let index = 0; index < givenIndex; index++) {
// this.__getIndexOffsetCalls++
indexSize = this.sizes.get(this.param.uniqueIds[index])
offset = offset + (typeof indexSize === 'number' ? indexSize : this.getEstimateSize())
}
// remember last calculate index
this.lastCalcIndex = Math.max(this.lastCalcIndex, givenIndex - 1)
this.lastCalcIndex = Math.min(this.lastCalcIndex, this.getLastIndex())
return offset
}
// is fixed size type
isFixedType() {
return this.calcType === CALC_TYPE.FIXED
}
// return the real last index
getLastIndex() {
return this.param.uniqueIds.length - 1
}
// in some conditions range is broke, we need correct it
// and then decide whether need update to next range
checkRange(start, end) {
const keeps = this.param.keeps
const total = this.param.uniqueIds.length
// datas less than keeps, render all
if (total <= keeps) {
start = 0
end = this.getLastIndex()
} else if (end - start < keeps - 1) {
// if range length is less than keeps, current it base on end
start = end - keeps + 1
}
if (this.range.start !== start) {
this.updateRange(start, end)
}
}
// setting to a new range and re-render
updateRange(start, end) {
this.range.start = start
this.range.end = end
this.range.padFront = this.getPadFront()
this.range.padBehind = this.getPadBehind()
this.callUpdate(this.getRange())
}
// return end base on start
getEndByStart(start) {
const theoryEnd = start + this.param.keeps - 1
const trulyEnd = Math.min(theoryEnd, this.getLastIndex())
return trulyEnd
}
// return total front offset
getPadFront() {
if (this.isFixedType()) {
return this.fixedSizeValue * this.range.start
} else {
return this.getIndexOffset(this.range.start)
}
}
// return total behind offset
getPadBehind() {
const end = this.range.end
const lastIndex = this.getLastIndex()
if (this.isFixedType()) {
return (lastIndex - end) * this.fixedSizeValue
}
// if it's all calculated, return the exactly offset
if (this.lastCalcIndex === lastIndex) {
return this.getIndexOffset(lastIndex) - this.getIndexOffset(end)
} else {
// if not, use a estimated value
return (lastIndex - end) * this.getEstimateSize()
}
}
// get the item estimate size
getEstimateSize() {
return this.isFixedType() ? this.fixedSizeValue : (this.firstRangeAverageSize || this.param.estimateSize)
}
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2017, 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.
*/
var path = require("path");
var fs = require('fs');
var protobuf = require('protobufjs');
var loaderUtils = require('loader-utils');
var jsonTarget = require('protobufjs/cli/targets/json');
module.exports = function(source) {
var self = this;
var root = new protobuf.Root();
var paths = loaderUtils.getOptions(this)['paths'] || [];
// Search include paths when resolving imports
root.resolvePath = function pbjsResolvePath(origin, target) {
var normOrigin = protobuf.util.path.normalize(origin);
var normTarget = protobuf.util.path.normalize(target);
var resolved = protobuf.util.path.resolve(normOrigin, normTarget, true);
if (fs.existsSync(resolved)) {
self.addDependency(resolved);
return resolved;
}
for (var i = 0; i < paths.length; ++i) {
var iresolved = protobuf.util.path.resolve(paths[i] + "/", target);
if (fs.existsSync(iresolved)) {
self.addDependency(iresolved);
return iresolved;
}
}
self.addDependency(resolved);
return resolved;
};
root.loadSync(self.resourcePath).resolveAll();
var result = JSON.stringify(root, null, 2);
return `module.exports = ${result}`;
};

View File

@@ -1,77 +0,0 @@
{
"name": "winscope",
"description": "A window manager analysis tool",
"version": "0.0.1",
"author": "Adrian Roos <roosa@google.com>",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development NODE_OPTIONS=--openssl-legacy-provider webpack serve --open --hot",
"build": "cross-env NODE_ENV=production NODE_OPTIONS=--openssl-legacy-provider webpack --progress",
"test": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --config webpack.spec.config.js && jasmine dist/bundleSpec.js"
},
"dependencies": {
"cross-env": "^7.0.3",
"html2canvas": "^1.4.1",
"jszip": "^3.6.0",
"kotlin": "^1.5.21",
"lodash.clonedeep": "^4.5.0",
"ts-loader": "^8.3.0",
"typescript": "^4.3.5",
"vue": "^2.6.14",
"vue-context": "^6.0.0",
"vue-gtag": "^1.16.1",
"vue-material": "^1.0.0-beta-14",
"//": [
"upgrade vue-material version to latest once the following bug in v15",
"is fixed: https://github.com/vuematerial/vue-material/issues/2285"
],
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/core": "^7.14.6",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.14.7",
"@babel/register": "^7.14.5",
"@jetbrains/kotlin-webpack-plugin": "^3.0.2",
"@testing-library/vue": "^5.8.1",
"@types/lodash": "^4.14.171",
"babel-loader": "^8.2.2",
"compression-webpack-plugin": "^6.1.1",
"cross-env": "^7.0.3",
"css-loader": "^5.2.7",
"eslint": "^7.30.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-vue": "^7.13.0",
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
"html-webpack-plugin": "4.5.2",
"husky": "^7.0.0",
"jasmine": "^3.8.0",
"lint-staged": "^11.0.1",
"loader-utils": "^2.0.0",
"mini-css-extract-plugin": "^1.6.2",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"protobufjs": "^6.11.3",
"source-map-loader": "^1.1.3",
"style-loader": "^2.0.0",
"ts-loader": "^8.3.0",
"typescript": "^4.3.5",
"uglifyjs-webpack-plugin": "^2.2.0",
"vue-loader": "^15.9.2",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.14",
"webpack": "^4.46.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.8.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,vue}": "eslint --cache --fix"
}
}

View File

@@ -1,270 +0,0 @@
import { DiffGenerator, DiffType } from "../src/utils/diff.js";
import { NodeBuilder, toPlainObject } from "./utils/tree.js";
const treeOne = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(3).build(),
new NodeBuilder().setId(4).build(),
]).build();
const treeTwo = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(3).setChildren([
new NodeBuilder().setId(5).build(),
]).build(),
new NodeBuilder().setId(4).build(),
]).build();
function checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree) {
const diffTree = new DiffGenerator(newTree)
.compareWith(oldTree)
.withUniqueNodeId(node => node.id)
.withModifiedCheck(() => false)
.generateDiffTree();
expect(diffTree).toEqual(expectedDiffTree);
}
describe("DiffGenerator", () => {
it("can generate a simple add diff", () => {
const oldTree = treeOne;
const newTree = treeTwo;
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(2).setDiffType(DiffType.NONE).build(),
new NodeBuilder().setId(3).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(5).setDiffType(DiffType.ADDED).build(),
]).build(),
new NodeBuilder().setId(4).setDiffType(DiffType.NONE).build(),
]).build()
);
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
it("can generate a simple delete diff", () => {
const oldTree = treeTwo;
const newTree = treeOne;
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(2).setDiffType(DiffType.NONE).build(),
new NodeBuilder().setId(3).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(5).setDiffType(DiffType.DELETED).build(),
]).build(),
new NodeBuilder().setId(4).setDiffType(DiffType.NONE).build(),
]).build()
);
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
it("can generate a simple move diff", () => {
const oldTree = treeTwo;
const newTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(3).build(),
new NodeBuilder().setId(4).setChildren([
new NodeBuilder().setId(5).build(),
]).build(),
]).build();
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(2).setDiffType(DiffType.NONE).build(),
new NodeBuilder().setId(3).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(5).setDiffType(DiffType.DELETED_MOVE).build(),
]).build(),
new NodeBuilder().setId(4).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(5).setDiffType(DiffType.ADDED_MOVE).build(),
]).build(),
]).build()
);
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
it("can generate a simple modified diff", () => {
const oldTree = new NodeBuilder().setId(1).setData("xyz").setChildren([
new NodeBuilder().setId(2).setData("abc").build(),
new NodeBuilder().setId(3).setData("123").build(),
]).build();
const newTree = new NodeBuilder().setId(1).setData("xyz").setChildren([
new NodeBuilder().setId(2).setData("def").build(),
new NodeBuilder().setId(3).setData("123").build(),
]).build();
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setData("xyz").setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(2).setData("def").setDiffType(DiffType.MODIFIED).build(),
new NodeBuilder().setId(3).setData("123").setDiffType(DiffType.NONE).build(),
]).build()
);
const diffTree = new DiffGenerator(newTree)
.compareWith(oldTree)
.withUniqueNodeId(node => node.id)
.withModifiedCheck(
(newNode, oldNode) => newNode.data != oldNode.data)
.generateDiffTree();
expect(diffTree).toEqual(expectedDiffTree);
});
it("can handle move and inner addition diff", () => {
const oldTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(3).setChildren([
new NodeBuilder().setId(4).build(),
]).build(),
]).build();
const newTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).setChildren([
new NodeBuilder().setId(4).setChildren([
new NodeBuilder().setId(5).build(),
]).build(),
]).build(),
new NodeBuilder().setId(3).build(),
]).build();
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(2).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(4).setDiffType(DiffType.ADDED_MOVE).setChildren([
new NodeBuilder().setId(5).setDiffType(DiffType.ADDED).build(),
]).build(),
]).build(),
new NodeBuilder().setId(3).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(4).setDiffType(DiffType.DELETED_MOVE).build(),
]).build(),
]).build()
);
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
it("can handle move within same level", () => {
const oldTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(3).build(),
]).build();
const newTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(3).build(),
new NodeBuilder().setId(2).build(),
]).build();
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(3).setDiffType(DiffType.NONE).build(),
new NodeBuilder().setId(2).setDiffType(DiffType.NONE).build(),
]).build()
);
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
it("can handle addition within middle of level", () => {
const oldTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(3).build(),
]).build();
const newTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(4).build(),
new NodeBuilder().setId(3).build(),
]).build();
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(2).setDiffType(DiffType.NONE).build(),
new NodeBuilder().setId(4).setDiffType(DiffType.ADDED).build(),
new NodeBuilder().setId(3).setDiffType(DiffType.NONE).build(),
]).build()
);
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
it("can handle deletion within middle of level", () => {
const oldTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(3).build(),
new NodeBuilder().setId(4).build(),
]).build();
const newTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(4).build(),
]).build();
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(2).setDiffType(DiffType.NONE).build(),
new NodeBuilder().setId(3).setDiffType(DiffType.DELETED).build(),
new NodeBuilder().setId(4).setDiffType(DiffType.NONE).build(),
]).build()
);
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
it("fully visits deletes nodes", () => {
const oldTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).setChildren([
new NodeBuilder().setId(3).setChildren([
new NodeBuilder().setId(4).build(),
]).build(),
]).build(),
]).build();
const newTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).build(),
new NodeBuilder().setId(3).setChildren([
new NodeBuilder().setId(4).build(),
]).build(),
]).build();
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(2).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(3).setDiffType(DiffType.DELETED_MOVE).setChildren([
new NodeBuilder().setId(4).setDiffType(DiffType.DELETED_MOVE).build(),
]).build(),
]).build(),
new NodeBuilder().setId(3).setDiffType(DiffType.ADDED_MOVE).setChildren([
new NodeBuilder().setId(4).setDiffType(DiffType.NONE).build(),
]).build(),
]).build()
);
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
it("preserves node chips", () => {
const oldTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).setChips(["CHIP2"]).build(),
new NodeBuilder().setId(3).build(),
]).build();
const newTree = new NodeBuilder().setId(1).setChildren([
new NodeBuilder().setId(2).setChips(["CHIP2"]).build(),
new NodeBuilder().setId(4).setChips(["CHIP4"]).build(),
]).build();
const expectedDiffTree = toPlainObject(
new NodeBuilder().setId(1).setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setId(2).setChips(["CHIP2"]).setDiffType(DiffType.NONE).build(),
new NodeBuilder().setId(3).setDiffType(DiffType.DELETED).build(),
new NodeBuilder().setId(4).setChips(["CHIP4"]).setDiffType(DiffType.ADDED).build(),
]).build()
);
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
});

View File

@@ -1,400 +0,0 @@
import fs from 'fs';
import path from 'path';
import {dataFile, decodeAndTransformProto, FILE_DECODERS, FILE_TYPES} from '../src/decode';
import {combineWmSfWithImeDataIfExisting} from '../src/ime_processing';
const inputMethodClientFile =
'../spec/traces/ime_processing_traces/input_method_clients_trace.winscope';
const inputMethodServiceFile =
'../spec/traces/ime_processing_traces/input_method_service_trace.winscope';
const inputMethodManagerServiceFile =
'../spec/traces/ime_processing_traces/input_method_manager_service_trace.winscope';
const sfFile = '../spec/traces/ime_processing_traces/sf_trace_for_ime.winscope';
const wmFile = '../spec/traces/ime_processing_traces/wm_trace_for_ime.winscope';
/**
* Copied from decode.js but with the blobUrl mocked (due to some issues faced
* with importing Blob, which is used in the decode.js version of this function)
*/
function protoDecoder(buffer, params, fileName, store) {
const transformed =
decodeAndTransformProto(buffer, params, store.displayDefaults);
// add tagGenerationTrace to dataFile for WM/SF traces so tags can be
// generated
var tagGenerationTrace = null;
if (params.type === FILE_TYPES.WINDOW_MANAGER_TRACE ||
params.type === FILE_TYPES.SURFACE_FLINGER_TRACE) {
tagGenerationTrace = transformed;
}
let data;
if (params.timeline) {
data = transformed.entries || transformed.children;
} else {
data = [transformed];
}
const blobUrl = '';
return dataFile(
fileName, data.map((x) => x.timestamp), data, blobUrl, params.type,
tagGenerationTrace);
}
describe('Ime Processing', () => {
it('can combine wm & sf properties into ime traces', () => {
const inputMethodClientBuffer = new Uint8Array(
fs.readFileSync(path.resolve(__dirname, inputMethodClientFile)));
const inputMethodServiceBuffer = new Uint8Array(
fs.readFileSync(path.resolve(__dirname, inputMethodServiceFile)));
const inputMethodManagerServiceBuffer = new Uint8Array(fs.readFileSync(
path.resolve(__dirname, inputMethodManagerServiceFile)));
const sfBuffer =
new Uint8Array(fs.readFileSync(path.resolve(__dirname, sfFile)));
const wmBuffer =
new Uint8Array(fs.readFileSync(path.resolve(__dirname, wmFile)));
// (buffer, params, fileName, store)
const store = {displayDefaults: true};
const inputMethodClientTrace = protoDecoder(
inputMethodClientBuffer,
FILE_DECODERS[FILE_TYPES.IME_TRACE_CLIENTS].decoderParams,
'input_method_clients_trace.winscope', store);
const inputMethodServiceTrace = protoDecoder(
inputMethodServiceBuffer,
FILE_DECODERS[FILE_TYPES.IME_TRACE_SERVICE].decoderParams,
'input_method_service_trace.winscope', store);
const inputMethodManagerServiceTrace = protoDecoder(
inputMethodManagerServiceBuffer,
FILE_DECODERS[FILE_TYPES.IME_TRACE_MANAGERSERVICE].decoderParams,
'input_method_manager_service_trace.winscope', store);
const wmTrace = protoDecoder(
wmBuffer, FILE_DECODERS[FILE_TYPES.WINDOW_MANAGER_TRACE].decoderParams,
'sf_trace.winscope', store);
const sfTrace = protoDecoder(
sfBuffer, FILE_DECODERS[FILE_TYPES.SURFACE_FLINGER_TRACE].decoderParams,
'wm_trace.winscope', store);
const dataFiles = {
'ImeTrace Clients': inputMethodClientTrace,
'ImeTrace InputMethodService': inputMethodServiceTrace,
'ImeTrace InputMethodManagerService': inputMethodManagerServiceTrace,
'WindowManagerTrace': wmTrace,
'SurfaceFlingerTrace': sfTrace,
};
combineWmSfWithImeDataIfExisting(dataFiles);
expect(dataFiles.length == 5);
const processedImeClientTrace = dataFiles['ImeTrace Clients'];
expect(processedImeClientTrace.type).toEqual('ImeTraceClients');
expect(processedImeClientTrace.data).toBeDefined();
expect(processedImeClientTrace.data[0].hasWmSfProperties).toBeTrue();
expect(processedImeClientTrace.data[1].wmProperties).toBeDefined();
expect(processedImeClientTrace.data[1].wmProperties.name)
.toEqual('0d0h8m22s938ms');
expect(processedImeClientTrace.data[1].wmProperties.focusedApp)
.toEqual(
'com.google.android.apps.messaging/.ui.search.ZeroStateSearchActivity');
expect(processedImeClientTrace.data[1].wmProperties.focusedActivity.token)
.toEqual("9d8c2ef");
expect(processedImeClientTrace.data[1].wmProperties.focusedActivity.layerId)
.toEqual(260);
expect(processedImeClientTrace.data[1].wmProperties.focusedWindow.token)
.toEqual("928b3d");
expect(processedImeClientTrace.data[1].wmProperties.focusedWindow.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedImeClientTrace.data[1].wmProperties.imeControlTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity");
expect(processedImeClientTrace.data[1].wmProperties.imeControlTarget.windowContainer.identifier.hashCode)
.toEqual(247026562);
expect(processedImeClientTrace.data[1].wmProperties.imeInputTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity");
expect(processedImeClientTrace.data[1].wmProperties.imeInputTarget.windowContainer.identifier.hashCode)
.toEqual(247026562);
expect(processedImeClientTrace.data[1].wmProperties.imeInsetsSourceProvider
.insetsSourceProvider).toBeDefined();
expect(processedImeClientTrace.data[1].wmProperties.imeLayeringTarget.windowContainer.identifier.title)
.toEqual("SnapshotStartingWindow for taskId=1393");
expect(processedImeClientTrace.data[1].wmProperties.imeLayeringTarget.windowContainer.identifier.hashCode)
.toEqual(222907471);
expect(
processedImeClientTrace.data[1].wmProperties.isInputMethodWindowVisible)
.toBeFalse();
expect(processedImeClientTrace.data[1].wmProperties.proto).toBeDefined();
expect(processedImeClientTrace.data[1].sfProperties.name)
.toEqual('0d0h8m22s942ms');
expect(processedImeClientTrace.data[1]
.sfProperties.isInputMethodSurfaceVisible)
.toEqual(false);
expect(processedImeClientTrace.data[1].sfProperties.imeContainer.id)
.toEqual(12);
expect(processedImeClientTrace.data[1].sfProperties.inputMethodSurface.id)
.toEqual(280);
expect(processedImeClientTrace.data[1].sfProperties.rect.label).toEqual(
"Surface(name=77f1069 InputMethod)/@0xb4afb8f - animation-leash of insets_animation#280");
expect(processedImeClientTrace.data[1].sfProperties.screenBounds)
.toBeDefined();
expect(processedImeClientTrace.data[1].sfProperties.z).toEqual(1);
expect(processedImeClientTrace.data[1].sfProperties.zOrderRelativeOfId)
.toEqual(115);
expect(processedImeClientTrace.data[1].sfProperties.focusedWindowRgba)
.toBeDefined();
expect(processedImeClientTrace.data[1].sfProperties.proto).toBeDefined();
expect(processedImeClientTrace.data[10].wmProperties).toBeDefined();
expect(processedImeClientTrace.data[10].wmProperties.name)
.toEqual('0d0h8m23s69ms');
expect(processedImeClientTrace.data[10].wmProperties.focusedApp)
.toEqual(
'com.google.android.apps.messaging/.ui.search.ZeroStateSearchActivity');
expect(processedImeClientTrace.data[10].wmProperties.focusedActivity.token)
.toEqual("9d8c2ef");
expect(processedImeClientTrace.data[10].wmProperties.focusedActivity.layerId)
.toEqual(260);
expect(processedImeClientTrace.data[10].wmProperties.focusedWindow.token)
.toEqual("928b3d");
expect(processedImeClientTrace.data[10].wmProperties.focusedWindow.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedImeClientTrace.data[10].wmProperties.imeControlTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity");
expect(processedImeClientTrace.data[10].wmProperties.imeControlTarget.windowContainer.identifier.hashCode)
.toEqual(247026562);
expect(processedImeClientTrace.data[10].wmProperties.imeInputTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity");
expect(processedImeClientTrace.data[10].wmProperties.imeInputTarget.windowContainer.identifier.hashCode)
.toEqual(247026562);
expect(processedImeClientTrace.data[10].wmProperties.imeInsetsSourceProvider
.insetsSourceProvider).toBeDefined();
expect(processedImeClientTrace.data[10].wmProperties.imeLayeringTarget.windowContainer.identifier.title)
.toEqual("SnapshotStartingWindow for taskId=1393");
expect(processedImeClientTrace.data[10].wmProperties.imeLayeringTarget.windowContainer.identifier.hashCode)
.toEqual(222907471);
expect(
processedImeClientTrace.data[10].wmProperties.isInputMethodWindowVisible)
.toBeFalse();
expect(processedImeClientTrace.data[10].wmProperties.proto).toBeDefined();
expect(processedImeClientTrace.data[10].sfProperties.name)
.toEqual('0d0h8m23s73ms');
expect(processedImeClientTrace.data[10]
.sfProperties.isInputMethodSurfaceVisible)
.toEqual(false);
expect(processedImeClientTrace.data[10].sfProperties.imeContainer.id)
.toEqual(12);
expect(processedImeClientTrace.data[10].sfProperties.inputMethodSurface.id)
.toEqual(280);
expect(processedImeClientTrace.data[10].sfProperties.rect.label).toEqual(
"Surface(name=77f1069 InputMethod)/@0xb4afb8f - animation-leash of insets_animation#280");
expect(processedImeClientTrace.data[10].sfProperties.screenBounds)
.toBeDefined();
expect(processedImeClientTrace.data[10].sfProperties.z).toEqual(1);
expect(processedImeClientTrace.data[10].sfProperties.zOrderRelativeOfId)
.toEqual(115);
expect(processedImeClientTrace.data[10].sfProperties.focusedWindowRgba)
.toBeDefined();
expect(processedImeClientTrace.data[10].sfProperties.proto).toBeDefined();
const processedInputMethodServiceTrace =
dataFiles['ImeTrace InputMethodService'];
expect(processedInputMethodServiceTrace.type)
.toEqual('ImeTrace InputMethodService');
expect(processedInputMethodServiceTrace.data[0].hasWmSfProperties)
.toBeTrue();
expect(processedInputMethodServiceTrace.data[1].wmProperties).toBeDefined();
expect(processedInputMethodServiceTrace.data[1].wmProperties.name)
.toEqual('0d0h8m23s6ms');
expect(processedInputMethodServiceTrace.data[1].wmProperties.focusedApp)
.toEqual(
'com.google.android.apps.messaging/.ui.search.ZeroStateSearchActivity');
expect(processedInputMethodServiceTrace.data[1].wmProperties.focusedActivity.token)
.toEqual("9d8c2ef");
expect(processedInputMethodServiceTrace.data[1].wmProperties.focusedActivity.layerId)
.toEqual(260);
expect(processedInputMethodServiceTrace.data[1].wmProperties.focusedWindow.token)
.toEqual("928b3d");
expect(processedInputMethodServiceTrace.data[1].wmProperties.focusedWindow.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedInputMethodServiceTrace.data[1].wmProperties.imeControlTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity");
expect(processedInputMethodServiceTrace.data[1].wmProperties.imeControlTarget.windowContainer.identifier.hashCode)
.toEqual(247026562);
expect(processedInputMethodServiceTrace.data[1].wmProperties.imeInputTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity");
expect(processedInputMethodServiceTrace.data[1].wmProperties.imeInputTarget.windowContainer.identifier.hashCode)
.toEqual(247026562);
expect(processedInputMethodServiceTrace.data[1].wmProperties.imeInsetsSourceProvider
.insetsSourceProvider).toBeDefined();
expect(processedInputMethodServiceTrace.data[1].wmProperties.imeLayeringTarget.windowContainer.identifier.title)
.toEqual("SnapshotStartingWindow for taskId=1393");
expect(processedInputMethodServiceTrace.data[1].wmProperties.imeLayeringTarget.windowContainer.identifier.hashCode)
.toEqual(222907471);
expect(
processedInputMethodServiceTrace.data[1].wmProperties.isInputMethodWindowVisible)
.toBeFalse();
expect(processedInputMethodServiceTrace.data[1].wmProperties.proto).toBeDefined();
expect(processedInputMethodServiceTrace.data[1].sfProperties.name)
.toEqual('0d0h8m23s26ms');
expect(processedInputMethodServiceTrace.data[1]
.sfProperties.isInputMethodSurfaceVisible)
.toEqual(false);
expect(processedInputMethodServiceTrace.data[1].sfProperties.imeContainer.id)
.toEqual(12);
expect(processedInputMethodServiceTrace.data[1].sfProperties.inputMethodSurface.id)
.toEqual(280);
expect(processedInputMethodServiceTrace.data[1].sfProperties.rect.label).toEqual(
"Surface(name=77f1069 InputMethod)/@0xb4afb8f - animation-leash of insets_animation#280");
expect(processedInputMethodServiceTrace.data[1].sfProperties.screenBounds)
.toBeDefined();
expect(processedInputMethodServiceTrace.data[1].sfProperties.z).toEqual(1);
expect(processedInputMethodServiceTrace.data[1]
.sfProperties.zOrderRelativeOfId)
.toEqual(115);
expect(
processedInputMethodServiceTrace.data[1].sfProperties.focusedWindowRgba)
.toBeDefined();
expect(processedInputMethodServiceTrace.data[1].sfProperties.proto)
.toBeDefined();
expect(processedInputMethodServiceTrace.data[10].wmProperties)
.toBeDefined();
expect(processedInputMethodServiceTrace.data[10].wmProperties.name)
.toEqual('0d0h8m23s186ms');
expect(processedInputMethodServiceTrace.data[10].wmProperties.focusedApp)
.toEqual(
'com.google.android.apps.messaging/.ui.search.ZeroStateSearchActivity');
expect(processedInputMethodServiceTrace.data[10].wmProperties.focusedActivity.token)
.toEqual("9d8c2ef");
expect(processedInputMethodServiceTrace.data[10].wmProperties.focusedActivity.layerId)
.toEqual(260);
expect(processedInputMethodServiceTrace.data[10].wmProperties.focusedWindow.token)
.toEqual("928b3d");
expect(processedInputMethodServiceTrace.data[10].wmProperties.focusedWindow.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedInputMethodServiceTrace.data[10].wmProperties.imeControlTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedInputMethodServiceTrace.data[10].wmProperties.imeControlTarget.windowContainer.identifier.hashCode)
.toEqual(9603901);
expect(processedInputMethodServiceTrace.data[10].wmProperties.imeInputTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedInputMethodServiceTrace.data[10].wmProperties.imeInputTarget.windowContainer.identifier.hashCode)
.toEqual(9603901);
expect(processedInputMethodServiceTrace.data[10].wmProperties.imeInsetsSourceProvider
.insetsSourceProvider).toBeDefined();
expect(processedInputMethodServiceTrace.data[10].wmProperties.imeLayeringTarget.windowContainer.identifier.title)
.toEqual("SnapshotStartingWindow for taskId=1393");
expect(processedInputMethodServiceTrace.data[10].wmProperties.imeLayeringTarget.windowContainer.identifier.hashCode)
.toEqual(222907471);
expect(
processedInputMethodServiceTrace.data[10].wmProperties.isInputMethodWindowVisible)
.toBeTrue();
expect(processedInputMethodServiceTrace.data[10].wmProperties.proto)
.toBeDefined();
expect(processedInputMethodServiceTrace.data[10].sfProperties.name)
.toEqual('0d0h8m23s173ms');
expect(processedInputMethodServiceTrace.data[10]
.sfProperties.isInputMethodSurfaceVisible)
.toEqual(false);
expect(processedInputMethodServiceTrace.data[10].sfProperties.imeContainer.id)
.toEqual(12);
expect(processedInputMethodServiceTrace.data[10].sfProperties.inputMethodSurface.id)
.toEqual(291);
expect(processedInputMethodServiceTrace.data[10].sfProperties.rect.label).toEqual(
"Surface(name=77f1069 InputMethod)/@0xb4afb8f - animation-leash of" +
" insets_animation#291");
expect(processedInputMethodServiceTrace.data[10].sfProperties.screenBounds)
.toBeDefined();
expect(processedInputMethodServiceTrace.data[10].sfProperties.z).toEqual(1);
expect(processedInputMethodServiceTrace.data[10]
.sfProperties.zOrderRelativeOfId)
.toEqual(260);
expect(processedInputMethodServiceTrace.data[10]
.sfProperties.focusedWindowRgba)
.toBeDefined();
expect(processedInputMethodServiceTrace.data[10].sfProperties.proto)
.toBeDefined();
const processedInputMethodManagerServiceTrace = dataFiles['ImeTrace' +
' InputMethodManagerService'];
expect(processedInputMethodManagerServiceTrace.type)
.toEqual(
'ImeTrace' +
' InputMethodManagerService');
expect(processedInputMethodManagerServiceTrace.data[0].hasWmSfProperties)
.toBeTrue();
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties)
.toBeDefined();
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.name)
.toEqual('0d0h8m23s52ms');
expect(
processedInputMethodManagerServiceTrace.data[1].wmProperties.focusedApp)
.toEqual(
'com.google.android.apps.messaging/.ui.search.ZeroStateSearchActivity');
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.focusedActivity.token)
.toEqual("9d8c2ef");
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.focusedActivity.layerId)
.toEqual(260);
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.focusedWindow.token)
.toEqual("928b3d");
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.focusedWindow.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.imeControlTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity");
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.imeControlTarget.windowContainer.identifier.hashCode)
.toEqual(247026562);
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.imeInputTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity");
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.imeInputTarget.windowContainer.identifier.hashCode)
.toEqual(247026562);
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.imeInsetsSourceProvider
.insetsSourceProvider).toBeDefined();
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.imeLayeringTarget.windowContainer.identifier.title)
.toEqual("SnapshotStartingWindow for taskId=1393");
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.imeLayeringTarget.windowContainer.identifier.hashCode)
.toEqual(222907471);
expect(
processedInputMethodManagerServiceTrace.data[1].wmProperties.isInputMethodWindowVisible)
.toBeFalse();
expect(processedInputMethodManagerServiceTrace.data[1].wmProperties.proto).toBeDefined();
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties)
.toBeDefined();
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.name)
.toEqual('0d0h8m27s309ms');
expect(processedInputMethodManagerServiceTrace.data[10]
.wmProperties.focusedApp)
.toEqual(
'com.google.android.apps.messaging/.ui.search.ZeroStateSearchActivity');
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.focusedActivity.token)
.toEqual("b1ce2cd");
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.focusedActivity.layerId)
.toEqual(305);
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.focusedWindow.token)
.toEqual("31d0b");
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.focusedWindow.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.imeControlTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.imeControlTarget.windowContainer.identifier.hashCode)
.toEqual(204043);
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.imeInputTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.imeInputTarget.windowContainer.identifier.hashCode)
.toEqual(204043);
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.imeInsetsSourceProvider
.insetsSourceProvider).toBeDefined();
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.imeLayeringTarget.windowContainer.identifier.title)
.toEqual("com.google.android.apps.messaging/com.google.android.apps.messaging.ui.search.ZeroStateSearchActivity");
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.imeLayeringTarget.windowContainer.identifier.hashCode)
.toEqual(204043);
expect(
processedInputMethodManagerServiceTrace.data[10].wmProperties.isInputMethodWindowVisible)
.toBeTrue();
expect(processedInputMethodManagerServiceTrace.data[10].wmProperties.proto).toBeDefined();
});
});

View File

@@ -1,183 +0,0 @@
import { DiffType } from "../src/utils/diff.js";
import { ObjectTransformer } from "../src/transform.js";
import { NodeBuilder, toPlainObject } from "./utils/tree.js";
describe("ObjectTransformer", () => {
it("can transform a simple object", () => {
const obj = {
obj: {
string: 'string',
number: 3,
},
array: [
{
nested: "item",
},
"two",
],
};
const expectedTransformedObj = toPlainObject(
new NodeBuilder().setTransformed().setName('root')
.setStableId('root').setChildren([
new NodeBuilder().setTransformed().setName('obj')
.setStableId('root.obj').setChildren([
new NodeBuilder().setTransformed().setName('string: string')
.setStableId('root.obj.string').setCombined().build(),
new NodeBuilder().setTransformed().setName('number: 3')
.setStableId('root.obj.number').setCombined().build(),
]).build(),
new NodeBuilder().setTransformed().setName('array')
.setStableId('root.array').setChildren([
new NodeBuilder().setTransformed().setName('0')
.setStableId('root.array.0').setChildren([
new NodeBuilder().setTransformed().setName('nested: item')
.setStableId('root.array.0.nested').setCombined().build(),
]).build(),
new NodeBuilder().setTransformed().setName("1: two")
.setStableId('root.array.1').setCombined().build(),
]).build()
]).build()
);
const transformedObj = new ObjectTransformer(obj, 'root', 'root')
.setOptions({ formatter: () => { } })
.transform();
expect(transformedObj).toEqual(expectedTransformedObj);
});
it("handles null as expected", () => {
const obj = {
obj: {
null: null,
},
}
const expectedTransformedObj = toPlainObject(
new NodeBuilder().setTransformed().setName('root')
.setStableId('root').setChildren([
new NodeBuilder().setTransformed().setName('obj')
.setStableId('root.obj').setChildren([
new NodeBuilder().setTransformed().setName('null: null')
.setStableId('root.obj.null').setCombined().build(),
]).build(),
]).build()
);
const transformedObj = new ObjectTransformer(obj, 'root', 'root')
.setOptions({ formatter: () => { } })
.transform();
expect(transformedObj).toEqual(expectedTransformedObj);
});
it("can generate a simple add diff", () => {
const oldObj = {
a: {
b: 1,
},
c: 2,
};
const newObj = {
a: {
b: 1,
d: 3,
},
c: 2,
};
const expectedTransformedObj = toPlainObject(
new NodeBuilder().setTransformed().setName('root')
.setStableId('root').setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setTransformed().setName('a')
.setStableId('root.a').setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setTransformed().setName('b: 1')
.setStableId('root.a.b').setDiffType(DiffType.NONE).setCombined().build(),
new NodeBuilder().setTransformed().setName('d: 3')
.setStableId('root.a.d').setDiffType(DiffType.ADDED).setCombined().build(),
]).build(),
new NodeBuilder().setTransformed().setName('c: 2').setStableId('root.c').setDiffType(DiffType.NONE).setCombined().build(),
]).build()
);
const transformedObj = new ObjectTransformer(newObj, 'root', 'root')
.setOptions({ formatter: () => { } })
.withDiff(oldObj)
.transform();
expect(transformedObj).toEqual(expectedTransformedObj);
});
it("can handle null", () => {
const oldObj = {
a: null,
};
const newObj = {
a: 1,
};
const expectedTransformedObj = toPlainObject(
new NodeBuilder().setTransformed().setName('root')
.setStableId('root').setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setTransformed().setName('a')
.setStableId('root.a').setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setTransformed().setName('1')
.setStableId('root.a.1').setDiffType(DiffType.ADDED).build(),
new NodeBuilder().setTransformed().setName('null')
.setStableId('root.a.null').setDiffType(DiffType.DELETED).build(),
]).build(),
]).build()
);
const transformedObj = new ObjectTransformer(newObj, 'root', 'root')
.setOptions({ formatter: () => { } })
.withDiff(oldObj)
.transform();
expect(transformedObj).toEqual(expectedTransformedObj);
});
it("can handle nested null", () => {
const oldObj = {
a: {
b: null,
},
c: 2,
};
const newObj = {
a: {
b: 1,
},
c: 2,
};
const expectedTransformedObj = toPlainObject(
new NodeBuilder().setTransformed().setName('root')
.setStableId('root').setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setTransformed().setName('a')
.setStableId('root.a').setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setTransformed().setName('b')
.setStableId('root.a.b').setDiffType(DiffType.NONE).setChildren([
new NodeBuilder().setTransformed().setName('1')
.setStableId('root.a.b.1').setDiffType(DiffType.ADDED).build(),
new NodeBuilder().setTransformed().setName('null')
.setStableId('root.a.b.null').setDiffType(DiffType.DELETED).build(),
]).build(),
]).build(),
new NodeBuilder().setTransformed().setName('c: 2')
.setStableId('root.c').setDiffType(DiffType.NONE).setCombined().build(),
]).build()
);
const transformedObj = new ObjectTransformer(newObj, 'root', 'root')
.setOptions({ formatter: () => { } })
.withDiff(oldObj)
.transform();
expect(transformedObj).toEqual(expectedTransformedObj);
});
});

View File

@@ -1,28 +0,0 @@
import { decodeAndTransformProto, FILE_TYPES, FILE_DECODERS } from '../src/decode';
import fs from 'fs';
import path from 'path';
import { expectedEntries, expectedLayers, layers_traces } from './traces/ExpectedTraces';
describe("Proto Transformations", () => {
it("can transform surface flinger traces", () => {
for (var i = 0; i < layers_traces.length; i++) {
const trace = layers_traces[i];
const buffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, trace)));
const data = decodeAndTransformProto(buffer, FILE_DECODERS[FILE_TYPES.SURFACE_FLINGER_TRACE].decoderParams, true);
// use final entry as this determines if there was any error in previous entry parsing
const transformedEntry = data.entries[data.entries.length-1];
const expectedEntry = expectedEntries[i];
for (const property in expectedEntry) {
expect(transformedEntry[property]).toEqual(expectedEntry[property]);
}
// check final flattened layer
const transformedLayer = transformedEntry.flattenedLayers[transformedEntry.flattenedLayers.length-1];
const expectedLayer = expectedLayers[i];
for (const property in expectedLayer) {
expect(transformedLayer[property]).toEqual(expectedLayer[property]);
}
}
});
});

View File

@@ -1,24 +0,0 @@
import { getSimplifiedLayerName } from "../src/utils/names";
const simplifications = {
"WindowToken{38eae45 android.os.BinderProxy@398bebc}#0": "WindowToken",
"7d8c460 NavigationBar0#0": "NavigationBar0#0",
"Surface(name=d2965b1 NavigationBar0)/@0xe4380b2 - animation-leash#2": "Surface - animation-leash#2",
"com.breel.wallpapers19.doodle.wallpaper.variations.DoodleWallpaperV1#0": "DoodleWallpaperV1#0",
"ActivityRecord{825ebe6 u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity#0": "ActivityRecord",
"com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0": "NexusLauncherActivity#0",
"com.android.settings/com.android.settings.Settings$UsbDetailsActivity#0": "Settings$UsbDetailsActivity#0",
"7d8c460 com.google.android.calendar/com.google.android.calendar.AllInOneCalendarActivity#0": "AllInOneCalendarActivity#0",
"WallpaperWindowToken{ad25afe token=android.os.Binder@8ab6b9}#0": "WallpaperWindowToken",
};
describe("getSimplifiedLayerName", () => {
it("simplifies traces as expected", () => {
for (const longName in simplifications) {
const expectedSimplifiedName = simplifications[longName];
const actualSimplifiedName = getSimplifiedLayerName(longName);
expect(actualSimplifiedName).toBe(expectedSimplifiedName);
}
});
});

View File

@@ -1,71 +0,0 @@
import { decodeAndTransformProto, FILE_TYPES, FILE_DECODERS } from '../src/decode';
import Tag from '../src/flickerlib/tags/Tag';
import Error from '../src/flickerlib/errors/Error';
import { TaggingEngine } from '../src/flickerlib/common.js';
import fs from 'fs';
import path from 'path';
const tagTrace = '../spec/traces/tag_trace.winscope';
const errorTrace = '../spec/traces/error_trace.winscope';
describe("Tag Transformation", () => {
it("can transform tag traces", () => {
const buffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, tagTrace)));
const data = decodeAndTransformProto(buffer, FILE_DECODERS[FILE_TYPES.TAG_TRACE].decoderParams, true);
expect(data.entries[0].timestamp.toString()).toEqual('159979677861');
expect(data.entries[1].timestamp.toString()).toEqual('161268519083');
expect(data.entries[2].timestamp.toString()).toEqual('161126718496');
expect(data.entries[3].timestamp.toString()).toEqual('161613497398');
expect(data.entries[4].timestamp.toString()).toEqual('161227062777');
expect(data.entries[5].timestamp.toString()).toEqual('161268519083');
expect(data.entries[6].timestamp.toString()).toEqual('161825945076');
expect(data.entries[7].timestamp.toString()).toEqual('162261072567');
expect(data.entries[0].tags).toEqual([new Tag(12345,"PIP_ENTER",true,1,"",0)]);
expect(data.entries[1].tags).toEqual([new Tag(12345,"PIP_ENTER",false,2,"",2)]);
expect(data.entries[2].tags).toEqual([new Tag(67890,"ROTATION",true,3,"",3)]);
expect(data.entries[3].tags).toEqual([new Tag(67890,"ROTATION",false,4,"",4)]);
expect(data.entries[4].tags).toEqual([new Tag(9876,"PIP_EXIT",true,5,"",5)]);
expect(data.entries[5].tags).toEqual([new Tag(9876,"PIP_EXIT",false,6,"",6)]);
expect(data.entries[6].tags).toEqual([new Tag(54321,"IME_APPEAR",true,7,"",7)]);
expect(data.entries[7].tags).toEqual([new Tag(54321,"IME_APPEAR",false,8,"",8)]);
})
});
describe("Detect Tag", () => {
it("can detect tags", () => {
const wmFile = '../spec/traces/regular_rotation_in_last_state_wm_trace.winscope'
const layersFile = '../spec/traces/regular_rotation_in_last_state_layers_trace.winscope'
const wmBuffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, wmFile)));
const layersBuffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, layersFile)));
const wmTrace = decodeAndTransformProto(wmBuffer, FILE_DECODERS[FILE_TYPES.WINDOW_MANAGER_TRACE].decoderParams, true);
const layersTrace = decodeAndTransformProto(layersBuffer, FILE_DECODERS[FILE_TYPES.SURFACE_FLINGER_TRACE].decoderParams, true);
const engine = new TaggingEngine(wmTrace, layersTrace, (text) => { console.log(text) });
const tagTrace = engine.run();
expect(tagTrace.size).toEqual(4);
expect(tagTrace.entries[0].timestamp.toString()).toEqual('280186737540384');
expect(tagTrace.entries[1].timestamp.toString()).toEqual('280187243649340');
expect(tagTrace.entries[2].timestamp.toString()).toEqual('280188522078113');
expect(tagTrace.entries[3].timestamp.toString()).toEqual('280189020672174');
})
});
describe("Error Transformation", () => {
it("can transform error traces", () => {
const buffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, errorTrace)));
const data = decodeAndTransformProto(buffer, FILE_DECODERS[FILE_TYPES.ERROR_TRACE].decoderParams, true);
expect(data.entries[0].timestamp.toString()).toEqual('161401263106');
expect(data.entries[1].timestamp.toString()).toEqual('161126718496');
expect(data.entries[2].timestamp.toString()).toEqual('162261072567');
expect(data.entries[0].errors).toEqual([new Error("","",33,"",33)]);
expect(data.entries[1].errors).toEqual([new Error("","",66,"",66)]);
expect(data.entries[2].errors).toEqual([new Error("","",99,"",99)]);
})
});

View File

@@ -1,11 +0,0 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"../node_modules/@babel/register/lib/node.js"
],
"stopSpecOnExpectationFailure": false,
"random": false
}

View File

@@ -1,492 +0,0 @@
import { ActiveBuffer, RectF, Transform, Matrix33, Color, Rect, Region } from '../../src/flickerlib/common.js';
import { VISIBLE_CHIP } from '../../src/flickerlib/treeview/Chips';
const standardTransform = new Transform(0, new Matrix33(1, 0, 0, 0, 1, 0));
const standardRect = new Rect(0, 0, 0, 0);
const standardColor = new Color(0, 0, 0, 1);
const standardCrop = new Rect(0, 0, -1, -1);
const expectedEmptyRegionLayer = {
backgroundBlurRadius: 0,
chips: [],
cornerRadius: 0,
effectiveScalingMode: 0,
hwcCompositionType: "INVALID",
id: 580,
isOpaque: false,
isRelativeOf: false,
kind: "580",
name: "SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0",
shadowRadius: 0,
shortName: "SurfaceView - com.android.(...).Main#0",
type: "BufferLayer",
z: -1,
zOrderRelativeOf: null,
parentId: 579,
activeBuffer: new ActiveBuffer(1440, 2614, 1472, 1),
bufferTransform: standardTransform,
color: new Color(0, 0, 0, 0.0069580078125),
crop: standardCrop,
hwcFrame: standardRect,
screenBounds: new RectF(37, 43, 146, 152),
transform: new Transform(0, new Matrix33(1, 0, 37.37078094482422, 0, 1, -3.5995326042175293)),
visibleRegion: new Region([new Rect(37, 43, 146, 152)]),
};
const emptyRegionProto = {
2: "\nparent=0\ntype=BufferLayer\nname=Display Root#0",
3: "\nparent=0\ntype=BufferLayer\nname=Display Overlays#0",
4: "\nparent=2\ntype=BufferLayer\nname=mBelowAppWindowsContainers#0",
5: "\nparent=2\ntype=BufferLayer\nname=com.android.server.wm.DisplayContent$TaskStackContainers@193aa46#0",
6: "\nparent=5\ntype=BufferLayer\nname=animationLayer#0",
7: "\nparent=5\ntype=BufferLayer\nname=splitScreenDividerAnchor#0",
8: "\nparent=2\ntype=BufferLayer\nname=mAboveAppWindowsContainers#0",
9: "\nparent=2\ntype=BufferLayer\nname=mImeWindowsContainers#0",
10: "\nparent=5\ntype=BufferLayer\nname=Stack=0#0",
11: "\nparent=10\ntype=ColorLayer\nname=animation background stackId=0#0",
12: "\nparent=9\ntype=BufferLayer\nname=WindowToken{f81e7fc android.os.Binder@7c880ef}#0",
13: "\nparent=4\ntype=BufferLayer\nname=WallpaperWindowToken{3756850 token=android.os.Binder@25b3e13}#0",
18: "\nparent=13\ntype=BufferLayer\nname=fd46a8e com.breel.wallpapers.dioramas.lagos.LagosWallpaperService#0",
19: "\nparent=18\ntype=BufferLayer\nname=com.breel.wallpapers.dioramas.lagos.LagosWallpaperService#0",
20: "\nparent=8\ntype=BufferLayer\nname=WindowToken{fc1aa98 android.os.BinderProxy@3517c7b}#0",
21: "\nparent=20\ntype=BufferLayer\nname=10022f1 DockedStackDivider#0",
22: "\nparent=8\ntype=BufferLayer\nname=WindowToken{49a6772 android.os.BinderProxy@7ba1c7d}#0",
23: "\nparent=22\ntype=BufferLayer\nname=56ef7c3 AssistPreviewPanel#0",
24: "\nparent=8\ntype=BufferLayer\nname=WindowToken{35f7d5c android.os.BinderProxy@8b38fcf}#0",
25: "\nparent=24\ntype=BufferLayer\nname=9029865 NavigationBar#0",
26: "\nparent=8\ntype=BufferLayer\nname=WindowToken{a9a69ab android.os.BinderProxy@f64ffa}#0",
27: "\nparent=26\ntype=BufferLayer\nname=5334808 StatusBar#0",
28: "\nparent=8\ntype=BufferLayer\nname=WindowToken{a63ca37 android.os.BinderProxy@435eb36}#0",
29: "\nparent=28\ntype=BufferLayer\nname=1a40ba4 ScreenDecorOverlay#0",
30: "\nparent=8\ntype=BufferLayer\nname=WindowToken{4ed84c2 android.os.BinderProxy@33d1d0d}#0",
31: "\nparent=30\ntype=BufferLayer\nname=7a0d2d3 ScreenDecorOverlayBottom#0",
32: "\nparent=25\ntype=BufferLayer\nname=NavigationBar#0",
33: "\nparent=27\ntype=BufferLayer\nname=StatusBar#0",
34: "\nparent=29\ntype=BufferLayer\nname=ScreenDecorOverlay#0",
35: "\nparent=31\ntype=BufferLayer\nname=ScreenDecorOverlayBottom#0",
36: "\nparent=10\ntype=BufferLayer\nname=Task=239#0",
37: "\nparent=632\ntype=BufferLayer\nname=AppWindowToken{188ce21 token=Token{824488 ActivityRecord{b0d882b u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t239}}}#0",
38: "\nparent=37\ntype=BufferLayer\nname=9f6e33d com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
44: "\nparent=37\ntype=BufferLayer\nname=81a00fc com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
88: "\nparent=5\ntype=BufferLayer\nname=Stack=2#0",
89: "\nparent=88\ntype=ColorLayer\nname=animation background stackId=2#0",
90: "\nparent=88\ntype=BufferLayer\nname=Task=241#0",
91: "\nparent=633\ntype=BufferLayer\nname=AppWindowToken{a9f5144 token=Token{f102257 ActivityRecord{3a0fd6 u0 com.android.chrome/com.google.android.apps.chrome.Main t241}}}#0",
96: "\nparent=91\ntype=BufferLayer\nname=87e310e com.android.chrome/com.google.android.apps.chrome.Main#0",
574: "\nparent=8\ntype=BufferLayer\nname=WindowToken{37eed7d android.os.Binder@6e217d4}#0",
579: "\nparent=96\ntype=BufferLayer\nname=com.android.chrome/com.google.android.apps.chrome.Main#0",
580: "\nparent=579\ntype=BufferLayer\nname=SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0",
581: "\nparent=579\ntype=ColorLayer\nname=Background for -SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0",
583: "\nparent=44\ntype=BufferLayer\nname=com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
629: "\nparent=38\ntype=BufferLayer\nname=com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#2",
632: "\nparent=6\ntype=BufferLayer\nname=Surface(name=AppWindowToken{188ce21 token=Token{824488 ActivityRecord{b0d882b u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t239}}})/@0x90c9c46 - animation-leash#1",
633: "\nparent=6\ntype=BufferLayer\nname=Surface(name=AppWindowToken{a9f5144 token=Token{f102257 ActivityRecord{3a0fd6 u0 com.android.chrome/com.google.android.apps.chrome.Main t241}}})/@0xd9b9374 - animation-leash#1"
};
const expectedEmptyRegion = {
chips: [],
proto: emptyRegionProto,
hwcBlob: "",
isVisible: true,
kind: "entry",
rects: [],
shortName: "0d0h38m28s521ms",
timestampMs: "2308521813510",
where: "",
name: "0d0h38m28s521ms",
stableId: "LayerTraceEntry",
visibleLayers: [],
};
const expectedInvalidLayerVisibilityLayer = {
backgroundBlurRadius: 0,
chips: [],
cornerRadius: 0,
effectiveScalingMode: 0,
hwcCompositionType: "INVALID",
id: 1536,
isOpaque: false,
isRelativeOf: false,
kind: "1536",
name: "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#2",
shadowRadius: 0,
shortName: "com.google.(...).NexusLauncherActivity#2",
type: "BufferLayer",
z: 0,
zOrderRelativeOf: null,
parentId: 1535,
stableId: "BufferLayer 1536 com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#2",
activeBuffer: new ActiveBuffer(1440, 2880, 1472, 1),
bufferTransform: standardTransform,
color: new Color(-1, -1, -1, 0),
hwcFrame: standardRect,
transform: standardTransform,
visibleRegion: new Region([standardRect]),
};
const invalidLayerVisibilityProto = {
2: "\nparent=0\ntype=BufferLayer\nname=Display Root#0",
3: "\nparent=0\ntype=BufferLayer\nname=Display Overlays#0",
4: "\nparent=2\ntype=BufferLayer\nname=mBelowAppWindowsContainers#0",
5: "\nparent=2\ntype=BufferLayer\nname=com.android.server.wm.DisplayContent$TaskStackContainers@4270eb4#0",
6: "\nparent=5\ntype=BufferLayer\nname=animationLayer#0",
7: "\nparent=5\ntype=BufferLayer\nname=boostedAnimationLayer#0",
8: "\nparent=5\ntype=BufferLayer\nname=homeAnimationLayer#0",
9: "\nparent=5\ntype=BufferLayer\nname=splitScreenDividerAnchor#0",
10: "\nparent=2\ntype=BufferLayer\nname=mAboveAppWindowsContainers#0",
11: "\nparent=2\ntype=BufferLayer\nname=mImeWindowsContainers#0",
12: "\nparent=5\ntype=BufferLayer\nname=Stack=0#0",
13: "\nparent=12\ntype=ColorLayer\nname=animation background stackId=0#0",
14: "\nparent=11\ntype=BufferLayer\nname=WindowToken{268fcff android.os.Binder@6688c1e}#0",
15: "\nparent=4\ntype=BufferLayer\nname=WallpaperWindowToken{6572e20 token=android.os.Binder@9543923}#0",
20: "\nparent=15\ntype=BufferLayer\nname=5e2e96f com.breel.wallpapers.dioramas.lagos.LagosWallpaperService#0",
21: "\nparent=20\ntype=BufferLayer\nname=com.breel.wallpapers.dioramas.lagos.LagosWallpaperService#0",
26: "\nparent=10\ntype=BufferLayer\nname=WindowToken{68e3f31 android.os.BinderProxy@f018fd8}#0",
27: "\nparent=26\ntype=BufferLayer\nname=2b80616 NavigationBar#0",
28: "\nparent=10\ntype=BufferLayer\nname=WindowToken{4e20cae android.os.BinderProxy@9086129}#0",
29: "\nparent=28\ntype=BufferLayer\nname=b09a4f StatusBar#0",
30: "\nparent=3\ntype=BufferLayer\nname=WindowToken{501e3b8 android.os.BinderProxy@238661b}#0",
31: "\nparent=30\ntype=BufferLayer\nname=d803591 ScreenDecorOverlay#0",
32: "\nparent=3\ntype=BufferLayer\nname=WindowToken{56d48f7 android.os.BinderProxy@f0f2cf6}#0",
33: "\nparent=32\ntype=BufferLayer\nname=1cd8364 ScreenDecorOverlayBottom#0",
35: "\nparent=29\ntype=BufferLayer\nname=StatusBar#0",
36: "\nparent=31\ntype=BufferLayer\nname=ScreenDecorOverlay#0",
37: "\nparent=33\ntype=BufferLayer\nname=ScreenDecorOverlayBottom#0",
38: "\nparent=12\ntype=BufferLayer\nname=Task=2#0",
39: "\nparent=38\ntype=BufferLayer\nname=AppWindowToken{215b919 token=Token{104a060 ActivityRecord{7e30c63 u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t2}}}#0",
821: "\nparent=14\ntype=BufferLayer\nname=5c937c8 InputMethod#0",
1078: "\nparent=10\ntype=BufferLayer\nname=WindowToken{7dc6283 android.os.BinderProxy@f83c532}#0",
1079: "\nparent=1078\ntype=BufferLayer\nname=32c0c00 AssistPreviewPanel#0",
1080: "\nparent=10\ntype=BufferLayer\nname=WindowToken{9f8a3df android.os.BinderProxy@825027e}#0",
1081: "\nparent=1080\ntype=BufferLayer\nname=26d9efb DockedStackDivider#0",
1403: "\nparent=10\ntype=BufferLayer\nname=WindowToken{dedcfff android.os.Binder@a80cb1e}#0",
1447: "\nparent=39\ntype=BufferLayer\nname=39ca531 com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
1454: "\nparent=27\ntype=BufferLayer\nname=NavigationBar#0",
1502: "\nparent=10\ntype=BufferLayer\nname=WindowToken{3ea357b android.os.Binder@6d9a90a}#0",
1505: "\nparent=1518\ntype=BufferLayer\nname=Task=623#0",
1506: "\nparent=1505\ntype=BufferLayer\nname=AppWindowToken{6deed44 token=Token{45cae57 ActivityRecord{7f14bd6 u0 com.android.server.wm.flicker.testapp/.SimpleActivity t623}}}#0",
1518: "\nparent=5\ntype=BufferLayer\nname=Stack=51#0",
1519: "\nparent=1518\ntype=ColorLayer\nname=animation background stackId=51#0",
1521: "\nparent=1506\ntype=BufferLayer\nname=496d52e com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp.SimpleActivity#0",
1534: "\nparent=1447\ntype=BufferLayer\nname=com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
1535: "\nparent=39\ntype=BufferLayer\nname=e280197 com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
1536: "\nparent=1535\ntype=BufferLayer\nname=com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#2",
};
const expectedInvalidLayerVisibility = {
chips: [],
proto: invalidLayerVisibilityProto,
hwcBlob: "",
isVisible: true,
kind: "entry",
rects: [],
shortName: "2d22h13m17s233ms",
timestampMs: "252797233543024",
where: "",
name: "2d22h13m17s233ms",
stableId: "LayerTraceEntry",
visibleLayers: [],
};
const expectedOrphanLayersLayer = {
backgroundBlurRadius: 0,
chips: [],
cornerRadius: 0,
effectiveScalingMode: 0,
hwcCompositionType: "INVALID",
id: 1012,
isOpaque: true,
isRelativeOf: false,
kind: "1012",
name: "SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0",
shadowRadius: 0,
shortName: "SurfaceView - com.android.(...).Main#0",
type: "BufferLayer",
z: -1,
zOrderRelativeOf: null,
parentId: 1011,
stableId: "BufferLayer 1012 SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0",
activeBuffer: new ActiveBuffer(1440, 2614, 1472, 1),
bufferTransform: standardTransform,
color: standardColor,
crop: standardCrop,
hwcFrame: standardRect,
screenBounds: new RectF(0, 98, 1440, 2712),
transform: new Transform(0, new Matrix33(1, 0, 0, 0, 1, 98)),
visibleRegion: new Region([new Rect(0, 98, 1440, 2712)]),
};
const expectedOrphanLayersProto = {
2: "\nparent=0\ntype=BufferLayer\nname=Display Root#0",
3: "\nparent=0\ntype=BufferLayer\nname=Display Overlays#0",
4: "\nparent=2\ntype=BufferLayer\nname=mBelowAppWindowsContainers#0",
5: "\nparent=2\ntype=BufferLayer\nname=com.android.server.wm.DisplayContent$TaskStackContainers@e7dd520#0",
6: "\nparent=5\ntype=BufferLayer\nname=animationLayer#0",
7: "\nparent=5\ntype=BufferLayer\nname=splitScreenDividerAnchor#0",
8: "\nparent=2\ntype=BufferLayer\nname=mAboveAppWindowsContainers#0",
9: "\nparent=2\ntype=BufferLayer\nname=mImeWindowsContainers#0",
10: "\nparent=5\ntype=BufferLayer\nname=Stack=0#0",
11: "\nparent=10\ntype=ColorLayer\nname=animation background stackId=0#0",
12: "\nparent=9\ntype=BufferLayer\nname=WindowToken{1350b6f android.os.Binder@d1b0e4e}#0",
13: "\nparent=4\ntype=BufferLayer\nname=WallpaperWindowToken{4537182 token=android.os.Binder@d87c4cd}#0",
18: "\nparent=13\ntype=BufferLayer\nname=8d26107 com.breel.wallpapers.dioramas.lagos.LagosWallpaperService#0",
19: "\nparent=18\ntype=BufferLayer\nname=com.breel.wallpapers.dioramas.lagos.LagosWallpaperService#0",
20: "\nparent=8\ntype=BufferLayer\nname=WindowToken{fba948d android.os.BinderProxy@756b124}#0",
21: "\nparent=20\ntype=BufferLayer\nname=dc26642 DockedStackDivider#0",
22: "\nparent=8\ntype=BufferLayer\nname=WindowToken{45663b4 android.os.BinderProxy@5273887}#0",
23: "\nparent=22\ntype=BufferLayer\nname=c617bdd AssistPreviewPanel#0",
24: "\nparent=8\ntype=BufferLayer\nname=WindowToken{ef90888 android.os.BinderProxy@9d4dc2b}#0",
25: "\nparent=24\ntype=BufferLayer\nname=1d24221 NavigationBar#0",
26: "\nparent=8\ntype=BufferLayer\nname=WindowToken{6b1dca9 android.os.BinderProxy@a53d830}#0",
27: "\nparent=26\ntype=BufferLayer\nname=eaca22e StatusBar#0",
28: "\nparent=8\ntype=BufferLayer\nname=WindowToken{72e584c android.os.BinderProxy@3ba407f}#0",
29: "\nparent=28\ntype=BufferLayer\nname=46af095 ScreenDecorOverlay#0",
30: "\nparent=8\ntype=BufferLayer\nname=WindowToken{bc659b android.os.BinderProxy@f1405aa}#0",
31: "\nparent=30\ntype=BufferLayer\nname=80ead38 ScreenDecorOverlayBottom#0",
33: "\nparent=27\ntype=BufferLayer\nname=StatusBar#0",
34: "\nparent=29\ntype=BufferLayer\nname=ScreenDecorOverlay#0",
35: "\nparent=31\ntype=BufferLayer\nname=ScreenDecorOverlayBottom#0",
36: "\nparent=10\ntype=BufferLayer\nname=Task=2#0",
37: "\nparent=36\ntype=BufferLayer\nname=AppWindowToken{5162f77 token=Token{ac99d76 ActivityRecord{7749795 u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t2}}}#0",
38: "\nparent=37\ntype=BufferLayer\nname=2c19e73 com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
41: "\nparent=25\ntype=BufferLayer\nname=NavigationBar#1",
43: "\nparent=37\ntype=BufferLayer\nname=2f0c80b com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
46: "\nparent=5\ntype=BufferLayer\nname=Stack=1#0",
47: "\nparent=46\ntype=ColorLayer\nname=animation background stackId=1#0",
48: "\nparent=46\ntype=BufferLayer\nname=Task=89#0",
49: "\nparent=48\ntype=BufferLayer\nname=AppWindowToken{1d514da token=Token{d36fe85 ActivityRecord{e0ec0ef u0 com.android.chrome/com.google.android.apps.chrome.Main t89}}}#0",
54: "\nparent=49\ntype=BufferLayer\nname=8ae6e06 com.android.chrome/com.google.android.apps.chrome.Main#0",
607: "\nparent=5\ntype=BufferLayer\nname=Stack=9#0",
608: "\nparent=607\ntype=ColorLayer\nname=animation background stackId=9#0",
609: "\nparent=607\ntype=BufferLayer\nname=Task=97#0",
615: "\nparent=609\ntype=BufferLayer\nname=AppWindowToken{28730c9 token=Token{4d768d0 ActivityRecord{faf093 u0 com.google.android.gm/.welcome.WelcomeTourActivity t97}}}#0",
616: "\nparent=615\ntype=BufferLayer\nname=44e6e5c com.google.android.gm/com.google.android.gm.welcome.WelcomeTourActivity#0",
679: "\nparent=12\ntype=BufferLayer\nname=2d0b1e4 InputMethod#0",
993: "\nparent=8\ntype=BufferLayer\nname=WindowToken{e425e58 android.os.Binder@6d9a73b}#0",
1011: "\nparent=54\ntype=BufferLayer\nname=com.android.chrome/com.google.android.apps.chrome.Main#0",
1012: "\nparent=1011\ntype=BufferLayer\nname=SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0",
1013: "\nparent=1011\ntype=ColorLayer\nname=Background for -SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0",
};
const expectedOrphanLayers = {
chips: [],
proto: expectedOrphanLayersProto,
hwcBlob: "",
isVisible: true,
kind: "entry",
rects: [],
shortName: "3d23h30m9s820ms",
timestampMs: "343809820196384",
where: "",
name: "3d23h30m9s820ms",
stableId: "LayerTraceEntry",
visibleLayers: [],
};
const expectedRootLayer = {
backgroundBlurRadius: 0,
cornerRadius: 0,
effectiveScalingMode: 0,
hwcCompositionType: "INVALID",
id: 12545,
isOpaque: true,
isRelativeOf: false,
kind: "12545",
name: "com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp.SimpleActivity#0",
shadowRadius: 0,
shortName: "com.android.(...).SimpleActivity#0",
type: "BufferQueueLayer",
z: 0,
zOrderRelativeOf: null,
parentId: 12541,
stableId: "BufferQueueLayer 12545 com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp.SimpleActivity#0",
activeBuffer: new ActiveBuffer(1440, 2960, 1472, 1),
chips: [VISIBLE_CHIP],
bufferTransform: standardTransform,
color: standardColor,
crop: new Rect(0, 0, 1440, 2960),
hwcFrame: standardRect,
screenBounds: new RectF(0, 0, 1440, 2960),
sourceBounds: new RectF(0, 0, 1440, 2960),
transform: standardTransform,
visibleRegion: new Region([new Rect(0, 0, 1440, 2960)]),
};
const expectedRootProto = {
2: "\nparent=-1\ntype=ContainerLayer\nname=Root#0",
3: "\nparent=2\ntype=ContainerLayer\nname=mWindowContainers#0",
4: "\nparent=2\ntype=ContainerLayer\nname=mOverlayContainers#0",
5: "\nparent=3\ntype=ContainerLayer\nname=mBelowAppWindowsContainers#0",
6: "\nparent=3\ntype=ContainerLayer\nname=com.android.server.wm.DisplayContent$TaskContainers@708b672#0",
7: "\nparent=6\ntype=ContainerLayer\nname=animationLayer#0",
8: "\nparent=6\ntype=ContainerLayer\nname=boostedAnimationLayer#0",
9: "\nparent=6\ntype=ContainerLayer\nname=homeAnimationLayer#0",
10: "\nparent=6\ntype=ContainerLayer\nname=splitScreenDividerAnchor#0",
11: "\nparent=3\ntype=ContainerLayer\nname=mAboveAppWindowsContainers#0",
12: "\nparent=3\ntype=ContainerLayer\nname=ImeContainer#0",
13: "\nparent=6\ntype=ContainerLayer\nname=Task=1#0",
18: "\nparent=5\ntype=ContainerLayer\nname=WallpaperWindowToken{4c3f8ef token=android.os.Binder@a0341ce}#0",
19: "\nparent=18\ntype=ContainerLayer\nname=aa9ba7e com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2#0",
20: "\nparent=19\ntype=BufferQueueLayer\nname=com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2#0",
23: "\nparent=11\ntype=ContainerLayer\nname=WindowToken{2e98b86 android.os.BinderProxy@6e5dbc8}#0",
24: "\nparent=23\ntype=ContainerLayer\nname=5976c47 NavigationBar0#0",
25: "\nparent=11\ntype=ContainerLayer\nname=WindowToken{525aa4 android.os.BinderProxy@df1e236}#0",
26: "\nparent=25\ntype=ContainerLayer\nname=986c00d NotificationShade#0",
27: "\nparent=11\ntype=ContainerLayer\nname=WindowToken{7ec5009 android.os.BinderProxy@de2add3}#0",
28: "\nparent=27\ntype=ContainerLayer\nname=3a0542f StatusBar#0",
31: "\nparent=-1\ntype=ContainerLayer\nname=WindowToken{eef604c android.os.BinderProxy@d3a687f}#0",
32: "\nparent=31\ntype=ContainerLayer\nname=20b5895 ScreenDecorOverlay#0",
33: "\nparent=-1\ntype=ContainerLayer\nname=WindowToken{4846f6f android.os.BinderProxy@39824e}#0",
34: "\nparent=33\ntype=ContainerLayer\nname=1d714 ScreenDecorOverlayBottom#0",
36: "\nparent=32\ntype=BufferQueueLayer\nname=ScreenDecorOverlay#0",
38: "\nparent=34\ntype=BufferQueueLayer\nname=ScreenDecorOverlayBottom#0",
40: "\nparent=28\ntype=BufferQueueLayer\nname=StatusBar#0",
43: "\nparent=12\ntype=ContainerLayer\nname=WindowToken{fa12db9 android.os.Binder@4b88380}#0",
46: "\nparent=13\ntype=ContainerLayer\nname=Task=4#0",
47: "\nparent=46\ntype=ContainerLayer\nname=ActivityRecord{99bbfb0 u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity#0",
54: "\nparent=24\ntype=BufferQueueLayer\nname=NavigationBar0#0",
71: "\nparent=43\ntype=ContainerLayer\nname=e8f94d2 InputMethod#0",
11499: "\nparent=47\ntype=ContainerLayer\nname=6737b79 com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
11501: "\nparent=-1\ntype=ContainerLayer\nname=Input Consumer recents_animation_input_consumer#2",
11759: "\nparent=6\ntype=ContainerLayer\nname=Task=873#0",
11760: "\nparent=11759\ntype=ContainerLayer\nname=Task=874#0",
11761: "\nparent=11760\ntype=ContainerLayer\nname=ActivityRecord{7398002 u0 com.android.server.wm.flicker.testapp/.ImeActivityAutoFocus#0",
11785: "\nparent=11761\ntype=ColorLayer\nname=Letterbox - right#0",
12131: "\nparent=11\ntype=ContainerLayer\nname=WindowToken{bbffcfd android.os.Binder@547b554}#0",
12379: "\nparent=47\ntype=ContainerLayer\nname=3f8f098 com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#0",
12412: "\nparent=11761\ntype=ContainerLayer\nname=edca7c6 com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp.ImeActivityAutoFocus#0",
12448: "\nparent=2147483645\ntype=ContainerLayer\nname=Surface(name=ActivityRecord{99bbfb0 u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity)/@0x2c3972c - animation-leash#0",
12449: "\nparent=2147483645\ntype=ContainerLayer\nname=Surface(name=ActivityRecord{fc16c94 u0 com.android.server.wm.flicker.testapp/.ImeActivity)/@0x7049863 - animation-leash#0",
12485: "\nparent=6\ntype=ContainerLayer\nname=Task=908#0",
12486: "\nparent=12485\ntype=ContainerLayer\nname=Task=909#0",
12487: "\nparent=12486\ntype=ContainerLayer\nname=ActivityRecord{4b3c5cb u0 com.android.server.wm.flicker.testapp/.ImeActivity#0",
12500: "\nparent=2147483645\ntype=ContainerLayer\nname=Surface(name=ActivityRecord{99bbfb0 u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity)/@0x2c3972c - animation-leash#0",
12501: "\nparent=2147483645\ntype=ContainerLayer\nname=Surface(name=ActivityRecord{4b3c5cb u0 com.android.server.wm.flicker.testapp/.ImeActivity)/@0x4ad47a1 - animation-leash#0",
12502: "\nparent=2147483645\ntype=ContainerLayer\nname=Surface(name=WallpaperWindowToken{4c3f8ef token=android.os.Binder@a0341ce})/@0xcde5e65 - animation-leash#0",
12511: "\nparent=12487\ntype=ColorLayer\nname=Letterbox - right#1",
12514: "\nparent=12487\ntype=ContainerLayer\nname=debe1ed com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp.ImeActivity#0",
12526: "\nparent=11\ntype=ContainerLayer\nname=WindowToken{6b7d663 android.os.BinderProxy@391f21d}#0",
12527: "\nparent=12526\ntype=ContainerLayer\nname=32aa260 AssistPreviewPanel#0",
12529: "\nparent=11\ntype=ContainerLayer\nname=WindowToken{31f7489 android.os.BinderProxy@67b1e53}#0",
12530: "\nparent=12529\ntype=ContainerLayer\nname=cbb28bc DockedStackDivider#0",
12536: "\nparent=6\ntype=ContainerLayer\nname=Task=910#0",
12537: "\nparent=12536\ntype=ContainerLayer\nname=Task=911#0",
12538: "\nparent=12537\ntype=ContainerLayer\nname=ActivityRecord{d3b8a44 u0 com.android.server.wm.flicker.testapp/.SimpleActivity#0",
12541: "\nparent=12538\ntype=ContainerLayer\nname=a3583c5 com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp.SimpleActivity#0",
12545: "\nparent=12541\ntype=BufferQueueLayer\nname=com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp.SimpleActivity#0",
2147483645: "\nparent=-1\ntype=\nname=Offscreen Root",
};
const expectedRoot = {
chips: [],
proto: expectedRootProto,
hwcBlob: "",
isVisible: true,
kind: "entry",
shortName: "0d1h46m19s146ms",
timestampMs: "6379146308030",
where: "",
name: "0d1h46m19s146ms",
stableId: "LayerTraceEntry",
};
const expectedRootAospLayer = {
backgroundBlurRadius: 0,
cornerRadius: 0,
effectiveScalingMode: 0,
hwcCompositionType: "INVALID",
id: 876,
isOpaque: false,
isRelativeOf: false,
kind: "876",
name: "com.android.launcher3/com.android.launcher3.Launcher#0",
shadowRadius: 0,
shortName: "com.android.(...).Launcher#0",
type: "BufferLayer",
z: 0,
zOrderRelativeOf: null,
parentId: 41,
activeBuffer: new ActiveBuffer(1440, 2880, 1472, 1),
bufferTransform: standardTransform,
chips: [VISIBLE_CHIP],
color: standardColor,
crop: new Rect(0, 0, 1440, 2880),
hwcFrame: standardRect,
screenBounds: new RectF(0, 0, 1440, 2880),
sourceBounds: new RectF(0, 0, 1440, 2880),
transform: standardTransform,
visibleRegion: new Region([new Rect(0, 0, 1440, 2880)]),
};
const expectedRootAospProto = {
2: "\nparent=-1\ntype=ContainerLayer\nname=Display Root#0",
3: "\nparent=-1\ntype=ContainerLayer\nname=Display Overlays#0",
4: "\nparent=2\ntype=ContainerLayer\nname=mBelowAppWindowsContainers#0",
5: "\nparent=2\ntype=ContainerLayer\nname=com.android.server.wm.DisplayContent$TaskStackContainers@d8077b3#0",
6: "\nparent=5\ntype=ContainerLayer\nname=animationLayer#0",
7: "\nparent=5\ntype=ContainerLayer\nname=boostedAnimationLayer#0",
8: "\nparent=5\ntype=ContainerLayer\nname=homeAnimationLayer#0",
9: "\nparent=5\ntype=ContainerLayer\nname=splitScreenDividerAnchor#0",
10: "\nparent=2\ntype=ContainerLayer\nname=mAboveAppWindowsContainers#0",
11: "\nparent=2\ntype=ContainerLayer\nname=mImeWindowsContainers#0",
12: "\nparent=5\ntype=ContainerLayer\nname=Stack=0#0",
13: "\nparent=12\ntype=ColorLayer\nname=animation background stackId=0#0",
18: "\nparent=4\ntype=ContainerLayer\nname=WallpaperWindowToken{5a7eaca token=android.os.Binder@438b635}#0",
23: "\nparent=10\ntype=ContainerLayer\nname=WindowToken{d19e48 android.os.BinderProxy@560ac3a}#0",
24: "\nparent=23\ntype=ContainerLayer\nname=b2a84e1 NavigationBar0#0",
25: "\nparent=10\ntype=ContainerLayer\nname=WindowToken{74d6851 android.os.BinderProxy@8b22adb}#0",
26: "\nparent=25\ntype=ContainerLayer\nname=16448b6 StatusBar#0",
27: "\nparent=-1\ntype=ContainerLayer\nname=WindowToken{624863c android.os.BinderProxy@975b02f}#0",
28: "\nparent=27\ntype=ContainerLayer\nname=cdb9fc5 ScreenDecorOverlay#0",
29: "\nparent=-1\ntype=ContainerLayer\nname=WindowToken{cb7204b android.os.BinderProxy@b8f3d1a}#0",
30: "\nparent=29\ntype=ContainerLayer\nname=ad1ca28 ScreenDecorOverlayBottom#0",
31: "\nparent=28\ntype=BufferLayer\nname=ScreenDecorOverlay#0",
32: "\nparent=30\ntype=BufferLayer\nname=ScreenDecorOverlayBottom#0",
33: "\nparent=18\ntype=ContainerLayer\nname=4f4b23b com.android.systemui.ImageWallpaper#0",
34: "\nparent=33\ntype=BufferLayer\nname=com.android.systemui.ImageWallpaper#0",
36: "\nparent=26\ntype=BufferLayer\nname=StatusBar#0",
37: "\nparent=12\ntype=ContainerLayer\nname=Task=144#0",
38: "\nparent=37\ntype=ContainerLayer\nname=AppWindowToken{54e2de0 token=Token{f4c5fe3 ActivityRecord{6a9dc12 u0 com.android.launcher3/.Launcher t144}}}#0",
40: "\nparent=-1\ntype=ContainerLayer\nname=Input Consumer recents_animation_input_consumer#1",
41: "\nparent=38\ntype=ContainerLayer\nname=418b5c0 com.android.launcher3/com.android.launcher3.Launcher#0",
45: "\nparent=11\ntype=ContainerLayer\nname=WindowToken{9158878 android.os.Binder@4f4a5db}#0",
46: "\nparent=24\ntype=BufferLayer\nname=NavigationBar0#0",
731: "\nparent=10\ntype=ContainerLayer\nname=WindowToken{c0ebbde android.os.BinderProxy@1af0e60}#0",
732: "\nparent=731\ntype=ContainerLayer\nname=b37d1bf AssistPreviewPanel#0",
733: "\nparent=10\ntype=ContainerLayer\nname=WindowToken{dc6b7ea android.os.BinderProxy@166b08c}#0",
734: "\nparent=733\ntype=ContainerLayer\nname=2a1cadb DockedStackDivider#0",
862: "\nparent=10\ntype=ContainerLayer\nname=WindowToken{f63efe6 android.os.Binder@d536e41}#0",
865: "\nparent=887\ntype=ContainerLayer\nname=Task=170#0",
866: "\nparent=865\ntype=ContainerLayer\nname=AppWindowToken{c829d40 token=Token{59970c3 ActivityRecord{36f2472 u0 com.android.server.wm.flicker.testapp/.PipActivity t170}}}#0",
871: "\nparent=866\ntype=ContainerLayer\nname=8153ff7 com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp.PipActivity#0",
876: "\nparent=41\ntype=BufferLayer\nname=com.android.launcher3/com.android.launcher3.Launcher#0",
887: "\nparent=5\ntype=ContainerLayer\nname=Stack=78#0",
888: "\nparent=887\ntype=ColorLayer\nname=animation background stackId=78#0",
};
const expectedRootAosp = {
chips: [],
proto: expectedRootAospProto,
hwcBlob: "",
isVisible: true,
kind: "entry",
shortName: "0d1h3m1s911ms",
timestampMs: "3781911657318",
where: "",
name: "0d1h3m1s911ms",
stableId: "LayerTraceEntry",
};
const expectedEntries = [
expectedEmptyRegion,
expectedInvalidLayerVisibility,
expectedOrphanLayers,
expectedRoot,
expectedRootAosp
];
const expectedLayers = [
expectedEmptyRegionLayer,
expectedInvalidLayerVisibilityLayer,
expectedOrphanLayersLayer,
expectedRootLayer,
expectedRootAospLayer
];
const layers_traces = [
'../spec/traces/layers_trace/layers_trace_emptyregion.pb',
'../spec/traces/layers_trace/layers_trace_invalid_layer_visibility.pb',
'../spec/traces/layers_trace/layers_trace_orphanlayers.pb',
'../spec/traces/layers_trace/layers_trace_root.pb',
'../spec/traces/layers_trace/layers_trace_root_aosp.pb',
];
export { expectedEntries, expectedLayers, layers_traces };

View File

@@ -1,119 +0,0 @@
class NodeBuilder {
constructor() {
this.isTransformed = false;
}
setTransformed() {
this.isTransformed = true;
return this;
}
setId(id) {
this.id = id;
this.chips = [];
this.combined = false;
return this;
}
setStableId(stableId) {
this.stableId = stableId;
return this;
}
setName(name) {
this.name = name;
return this;
}
setData(data) {
this.data = data;
return this;
}
setChips(chips) {
this.chips = chips;
return this;
}
setCombined() {
this.combined = true;
return this;
}
setDiffType(diffType) {
this.diffType = diffType;
return this;
}
setChildren(children) {
this.children = children;
return this;
}
build() {
var node = {
name: undefined,
shortName: undefined,
stableId: undefined,
kind: undefined,
};
if (this.isTransformed)
{
delete node.shortName;
node.kind = ''
}
if ('id' in this) {
node.id = this.id;
}
if ('stableId' in this) {
node.stableId = this.stableId;
}
if ('name' in this) {
node.name = this.name;
}
if ('data' in this) {
node.data = this.data;
}
if ('chips' in this) {
node.chips = this.chips;
}
if (this.combined) {
node.combined = true;
}
if ('diffType' in this) {
node.diff = { type: this.diffType };
}
node.children = 'children' in this ? this.children : [];
return node;
}
}
function isPrimitive(test) {
return test !== Object(test);
};
function toPlainObject(theClass) {
if (isPrimitive(theClass)) {
return theClass;
} else if (Array.isArray(theClass)) {
return theClass.map(item => toPlainObject(item));
} else {
const keys = Object.getOwnPropertyNames(Object.assign({}, theClass));
return keys.reduce((classAsObj, key) => {
classAsObj[key] = toPlainObject(theClass[key]);
return classAsObj;
}, {});
}
}
export { NodeBuilder, toPlainObject };

View File

@@ -1,42 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<TraceView
:store="store"
:file="file"
:summarizer="summarizer"
:presentTags="[]"
:presentErrors="[]"
:propertyGroups="false"
/>
</template>
<script>
import TraceView from "@/TraceView.vue"
export default {
name: "AccessibilityTraceView",
props: ["store", "file"],
components: {
TraceView
},
methods: {
summarizer(item) {
return null;
},
}
}
</script>

View File

@@ -1,552 +0,0 @@
<!-- Copyright (C) 2017 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.
-->
<template>
<div id="app">
<vue-title :appName="appName" :traceName="traceNameForTitle" />
<md-dialog-prompt
class="edit-trace-name-dialog"
:md-active.sync="editingTraceName"
v-model="traceName"
md-title="Edit trace name"
md-input-placeholder="Enter a new trace name"
md-confirm-text="Save" />
<md-app>
<md-app-toolbar md-tag="md-toolbar" class="top-toolbar">
<h1 class="md-title">{{appName}} <span>Legacy</span></h1>
<div class="trace-name" v-if="dataLoaded">
<div>
<span>{{ traceName }}</span>
<!-- <input type="text" class="trace-name-editable" v-model="traceName" /> -->
<md-icon class="edit-trace-name-btn" @click.native="editTraceName()">edit</md-icon>
</div>
</div>
<md-button
class="md-primary md-theme-default download-all-btn"
@click="generateTags()"
v-if="dataLoaded && canGenerateTags"
>Generate Tags</md-button>
<md-button
class="md-primary md-theme-default"
@click="downloadAsZip(files, traceName)"
v-if="dataLoaded"
>Download All</md-button>
<md-button
class="md-accent md-raised md-theme-default clear-btn"
style="box-shadow: none;"
@click="clear()"
v-if="dataLoaded"
>Clear</md-button>
</md-app-toolbar>
<md-app-content class="main-content" :style="mainContentStyle">
<section class="use-new-winscope-banner" v-if="!dataLoaded">
<icon>🚀</icon>
<h2>New Winscope<span>beta</span></br>is available!</h2>
<a href="http://go/winscope-beta">
<md-button class="md-primary">try it now!</md-button>
</a>
</section>
<section class="data-inputs" v-if="!dataLoaded">
<div class="input">
<dataadb class="adbinput" ref="adb" :store="store"
@dataReady="onDataReady" />
</div>
<div class="input" @dragover.prevent @drop.prevent>
<datainput class="fileinput" ref="input" :store="store"
@dataReady="onDataReady" />
</div>
</section>
<section class="data-view">
<div
class="data-view-container"
v-for="file in dataViewFiles"
:key="file.type"
>
<dataview
:ref="file.type"
:store="store"
:file="file"
:presentTags="Object.freeze(presentTags)"
:presentErrors="Object.freeze(presentErrors)"
:dataViewFiles="dataViewFiles"
@click="onDataViewFocus(file)"
/>
</div>
<overlay
:presentTags="Object.freeze(presentTags)"
:presentErrors="Object.freeze(presentErrors)"
:store="store"
:ref="overlayRef"
:searchTypes="searchTypes"
v-if="dataLoaded"
v-on:bottom-nav-height-change="handleBottomNavHeightChange"
/>
</section>
</md-app-content>
</md-app>
</div>
</template>
<script>
import Overlay from './Overlay.vue';
import DataView from './DataView.vue';
import DataInput from './DataInput.vue';
import LocalStore from './localstore.js';
import DataAdb from './DataAdb.vue';
import FileType from './mixins/FileType.js';
import SaveAsZip from './mixins/SaveAsZip';
import FocusedDataViewFinder from './mixins/FocusedDataViewFinder';
import {DIRECTION} from './utils/utils';
import Searchbar from './Searchbar.vue';
import {NAVIGATION_STYLE, SEARCH_TYPE} from './utils/consts';
import {TRACE_TYPES, FILE_TYPES, dataFile} from './decode.js';
import { TaggingEngine } from './flickerlib/common';
import titleComponent from './Title.vue';
import BetaFeaturesToolbar from '@/BetaFeaturesToolbar';
const APP_NAME = 'Winscope';
const CONTENT_BOTTOM_PADDING = 25;
export default {
name: 'app',
mixins: [FileType, SaveAsZip, FocusedDataViewFinder],
data() {
return {
appName: APP_NAME,
activeDataView: null,
// eslint-disable-next-line new-cap
store: LocalStore('app', {
flattened: false,
onlyVisible: false,
simplifyNames: true,
displayDefaults: true,
navigationStyle: NAVIGATION_STYLE.GLOBAL,
flickerTraceView: false,
showFileTypes: [],
isInputMode: false,
betaFeatures: {
newImePanels: false,
},
}),
overlayRef: 'overlay',
mainContentStyle: {
'padding-bottom': `${CONTENT_BOTTOM_PADDING}px`,
},
tagFile: null,
presentTags: [],
presentErrors: [],
searchTypes: [SEARCH_TYPE.TIMESTAMP],
hasTagOrErrorTraces: false,
traceName: "unnamed_winscope_trace",
editingTraceName: false,
};
},
created() {
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('scroll', this.onScroll);
// document.title = this.traceName;
},
destroyed() {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('scroll', this.onScroll);
},
methods: {
/** Get states from either tag files or error files */
getUpdatedStates(files) {
var states = [];
for (const file of files) {
states.push(...file.data);
}
return states;
},
/** Get tags from all uploaded tag files*/
getUpdatedTags() {
if (this.tagFile === null) return [];
const tagStates = this.getUpdatedStates([this.tagFile]);
var tags = [];
tagStates.forEach(tagState => {
tagState.tags.forEach(tag => {
tag.timestamp = Number(tagState.timestamp);
// tags generated on frontend have transition.name due to kotlin enum
tag.transition = tag.transition.name ?? tag.transition;
tags.push(tag);
});
});
return tags;
},
/** Get tags from all uploaded error files*/
getUpdatedErrors() {
var errorStates = this.getUpdatedStates(this.errorFiles);
var errors = [];
//TODO (b/196201487) add check if errors empty
errorStates.forEach(errorState => {
errorState.errors.forEach(error => {
error.timestamp = Number(errorState.timestamp);
errors.push(error);
});
});
return errors;
},
/** Set flicker mode check for if there are tag/error traces uploaded*/
updateHasTagOrErrorTraces() {
return this.hasTagTrace() || this.hasErrorTrace();
},
hasTagTrace() {
return this.tagFile !== null;
},
hasErrorTrace() {
return this.errorFiles.length > 0;
},
/** Activate flicker search tab if tags/errors uploaded*/
updateSearchTypes() {
this.searchTypes = [SEARCH_TYPE.TIMESTAMP];
if (this.hasTagTrace()) {
this.searchTypes.push(SEARCH_TYPE.TRANSITIONS);
}
if (this.hasErrorTrace()) {
this.searchTypes.push(SEARCH_TYPE.ERRORS);
}
},
/** Filter data view files by current show settings*/
updateShowFileTypes() {
this.store.showFileTypes = this.dataViewFiles
.filter((file) => file.show)
.map(file => file.type);
},
clear() {
this.store.showFileTypes = [];
this.tagFile = null;
this.$store.commit('clearFiles');
this.recordButtonClickedEvent("Clear")
},
onDataViewFocus(file) {
this.$store.commit('setActiveFile', file);
this.activeDataView = file.type;
},
onKeyDown(event) {
event = event || window.event;
if (this.store.isInputMode) return false;
if (event.keyCode == 37 /* left */ ) {
this.$store.dispatch('advanceTimeline', DIRECTION.BACKWARD);
} else if (event.keyCode == 39 /* right */ ) {
this.$store.dispatch('advanceTimeline', DIRECTION.FORWARD);
} else if (event.keyCode == 38 /* up */ ) {
this.$refs[this.activeView][0].arrowUp();
} else if (event.keyCode == 40 /* down */ ) {
this.$refs[this.activeView][0].arrowDown();
} else {
return false;
}
event.preventDefault();
return true;
},
onDataReady(traceName, files) {
this.traceName = traceName;
this.$store.dispatch('setFiles', files);
this.tagFile = this.tagFiles[0] ?? null;
this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces();
this.presentTags = this.getUpdatedTags();
this.presentErrors = this.getUpdatedErrors();
this.updateSearchTypes();
this.updateFocusedView();
this.updateShowFileTypes();
},
setStatus(status) {
if (status) {
this.title = status;
} else {
this.title = APP_NAME;
}
},
handleBottomNavHeightChange(newHeight) {
this.$set(
this.mainContentStyle,
'padding-bottom',
`${ CONTENT_BOTTOM_PADDING + newHeight }px`,
);
},
generateTags() {
// generate tag file
this.recordButtonClickedEvent("Generate Tags");
const engine = new TaggingEngine(
this.$store.getters.tagGenerationWmTrace,
this.$store.getters.tagGenerationSfTrace,
(text) => { console.log(text) }
);
const tagTrace = engine.run();
const tagFile = this.generateTagFile(tagTrace);
// update tag trace in set files, update flicker mode
this.tagFile = tagFile;
this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces();
this.presentTags = this.getUpdatedTags();
this.presentErrors = this.getUpdatedErrors();
this.updateSearchTypes();
},
generateTagFile(tagTrace) {
const data = tagTrace.entries;
const blobUrl = URL.createObjectURL(new Blob([], {type: undefined}));
return dataFile(
"GeneratedTagTrace.winscope",
data.map((x) => x.timestamp),
data,
blobUrl,
FILE_TYPES.TAG_TRACE
);
},
editTraceName() {
this.editingTraceName = true;
},
setBetaImePanelFlag(flag) {
console.log('Beta IME feature (combining WM&SF properties in)' +
' enabled:', flag);
this.store.betaFeatures.newImePanels = flag;
},
},
computed: {
files() {
return this.$store.getters.sortedFiles.map(file => {
if (this.hasDataView(file)) {
file.show = true;
}
return file;
});
},
prettyDump() {
return JSON.stringify(this.dump, null, 2);
},
dataLoaded() {
return this.files.length > 0;
},
activeView() {
if (!this.activeDataView && this.files.length > 0) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.activeDataView = this.files[0].type;
}
return this.activeDataView;
},
dataViewFiles() {
return this.files.filter((file) => this.hasDataView(file));
},
tagFiles() {
return this.$store.getters.tagFiles;
},
errorFiles() {
return this.$store.getters.errorFiles;
},
timelineFiles() {
return this.$store.getters.timelineFiles;
},
canGenerateTags() {
const fileTypes = this.dataViewFiles.map((file) => file.type);
return fileTypes.includes(TRACE_TYPES.WINDOW_MANAGER)
&& fileTypes.includes(TRACE_TYPES.SURFACE_FLINGER);
},
traceNameForTitle() {
if (!this.dataLoaded) {
return undefined;
} else {
return this.traceName;
}
}
},
watch: {
// title() {
// document.title = this.title;
// },
},
components: {
overlay: Overlay,
dataview: DataView,
datainput: DataInput,
dataadb: DataAdb,
searchbar: Searchbar,
["vue-title"]: titleComponent,
betafeatures: BetaFeaturesToolbar,
},
};
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@600&display=swap');
#app .md-app-container {
/* Get rid of transforms which prevent fixed position from being used */
transform: none!important;
min-height: 100vh;
}
#app .top-toolbar {
box-shadow: none;
background-color: #fff;
background-color: var(--md-theme-default-background, #fff);
border-bottom: thin solid rgba(0,0,0,.12);
padding: 0 40px;
}
#app .top-toolbar .md-title {
font-family: 'Open Sans', sans-serif;
white-space: nowrap;
color: #5f6368;
margin: 0;
padding: 0;
font-size: 22px;
letter-spacing: 0;
font-weight: 600;
}
#app .top-toolbar .md-title span {
color: rgba(95, 99, 104, 0.5);
}
.data-view {
display: flex;
flex-direction: column;
}
.card-toolbar {
border-bottom: 1px solid rgba(0, 0, 0, .12);
}
.timeline {
margin: 16px;
}
.container {
display: flex;
flex-wrap: wrap;
}
.md-button {
margin-top: 1em
}
h1 {
font-weight: normal;
}
.data-inputs {
display: flex;
flex-wrap: wrap;
height: 100%;
width: 100%;
align-self: center;
/* align-items: center; */
align-content: center;
justify-content: center;
}
.data-inputs .input {
padding: 15px;
flex: 1 1 0;
max-width: 840px;
/* align-self: center; */
}
.data-inputs .input > div {
height: 100%;
}
.data-view-container {
padding: 25px 20px 0 20px;
}
.snackbar-break-words {
/* These are technically the same, but use both */
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-word;
/* Adds a hyphen where the word breaks, if supported (No Blink) */
-ms-hyphens: auto;
-moz-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
padding: 10px 10px 10px 10px;
}
.trace-name {
flex: 1;
display: flex;
align-items: center;
align-content: center;
justify-content: center;
font-family: 'Open Sans', sans-serif;
font-size: 1rem;
}
.md-icon.edit-trace-name-btn {
color: rgba(0, 0, 0, 0.6)!important;
font-size: 1rem!important;
margin-bottom: 0.1rem;
}
.md-icon.edit-trace-name-btn:hover {
cursor: pointer;
}
.trace-name-editable {
all: unset;
cursor: default;
}
.edit-trace-name-dialog .md-dialog-container {
min-width: 350px;
}
.md-overlay.md-dialog-overlay {
z-index: 10;
}
section.use-new-winscope-banner {
display: flex;
align-content: center;
align-items: center;
justify-content: center;
padding: 1rem 1rem calc(1rem + 16px) 1rem;
border-bottom: thin solid rgba(0,0,0,.12);;
margin-bottom: 1rem;
}
section.use-new-winscope-banner h2 {
font-weight: 300;
margin: 1rem;
}
section.use-new-winscope-banner icon {
font-size: 2rem;
}
section.use-new-winscope-banner span {
font-size: 1rem;
color: rgba(0, 0, 0, 0.5);
margin-bottom: -1rem;
display: inline-block;
transform: translateY(-10px);
padding: 2px;
}
</style>

View File

@@ -1,60 +0,0 @@
<template>
<div class="beta-options md-toolbar md-transparent md-dense">
<div>
<h2 class="heading md-title">Beta Features</h2>
<md-checkbox
v-model="betaImePanel">
IME Panel with WM & SF Properties
</md-checkbox>
</div>
<div class="description">
These features are still in beta and you may experience
some issues with them. Please select / unselect the checkbox(es)
BEFORE capturing with ADB or uploading your files.
</div>
</div>
</template>
<script>
export default {
name: 'BetaFeaturesToolbar.vue',
props: ['setBetaImePanelFlag'],
data() {
return {
betaImePanel: false,
};
},
created() {
// reset store's flag for use case where user clicks 'Clear'
this.setBetaImePanelFlag(this.betaImePanel);
},
watch: {
betaImePanel() {
this.setBetaImePanelFlag(this.betaImePanel);
},
},
};
</script>
<style scoped>
.beta-options {
border-bottom: 1px solid rgba(0, 0, 0, .12);
flex-direction: column !important;
align-content: flex-start !important;
align-items: flex-start !important;
}
.beta-options .heading {
padding-right: 15px;
border-right: 1px solid rgba(0, 0, 0, 0.12);
}
.beta-options .md-checkbox {
margin: 4px 10px;
padding-bottom: 7px;
}
.beta-options .description {
margin: 6px 8px 12px 8px;
}
</style>

View File

@@ -1,73 +0,0 @@
<!-- 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.
-->
<template>
<span class="coord-null-value" v-if="!hasCoordinates">null</span>
<div class="coord-table-wrapper" v-else>
<md-table class="table">
<md-table-row class="header-row">
<md-table-cell>Left</md-table-cell>
<md-table-cell>Top</md-table-cell>
<md-table-cell>Right</md-table-cell>
<md-table-cell>Bottom</md-table-cell>
</md-table-row>
<md-table-row>
<md-table-cell>{{ coordinates.left }}</md-table-cell>
<md-table-cell>{{ coordinates.top }}</md-table-cell>
<md-table-cell>{{ coordinates.right }}</md-table-cell>
<md-table-cell>{{ coordinates.bottom }}</md-table-cell>
</md-table-row>
</md-table>
</div>
</template>
<script>
export default {
name: 'CoordinatesTable.vue',
props: ['coordinates'],
computed: {
hasCoordinates() {
return this.coordinates.left || this.coordinates.top ||
this.coordinates.right || this.coordinates.bottom;
},
},
};
</script>
<style>
.coord-null-value {
color: rgba(0, 0, 0, 0.75);
}
.coord-table-wrapper {
margin-left: 10px;
display: inline-flex;
padding: 3px 0px;
}
.coord-table-wrapper .md-table-cell {
height: auto;
border: 1px solid rgba(0, 0, 0, 0.12);
}
.coord-table-wrapper .md-table-cell-container {
padding: 3px 15px;
text-align: center;
}
.coord-table-wrapper .header-row .md-table-cell {
color: gray;
font-weight: 600;
}
</style>

View File

@@ -1,449 +0,0 @@
<!-- Copyright (C) 2019 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.
-->
<template>
<flat-card style="min-width: 50em">
<md-card-header>
<div class="md-title">ADB Connect</div>
</md-card-header>
<md-card-content v-if="status === STATES.CONNECTING">
<md-progress-spinner md-indeterminate></md-progress-spinner>
</md-card-content>
<md-card-content v-if="status === STATES.NO_PROXY">
<md-icon class="md-accent">error</md-icon>
<span class="md-subheading">Unable to connect to Winscope ADB proxy</span>
<div class="md-body-2">
<p>Launch the Winscope ADB Connect proxy to capture traces directly from your browser.</p>
<p>Python 3.5+ and ADB is required.</p>
<p>Run:</p>
<pre>python3 $ANDROID_BUILD_TOP/development/tools/winscope/adb_proxy/winscope_proxy.py</pre>
<p>Or get it from the AOSP repository.</p>
</div>
<div class="md-layout">
<md-button class="md-accent" :href="downloadProxyUrl" @click="buttonClicked(`Download from AOSP`)">Download from AOSP</md-button>
<md-button class="md-accent" @click="restart">Retry</md-button>
</div>
</md-card-content>
<md-card-content v-if="status === STATES.INVALID_VERSION">
<md-icon class="md-accent">update</md-icon>
<span class="md-subheading">The version of Winscope ADB Connect proxy running on your machine is incopatibile with Winscope.</span>
<div class="md-body-2">
<p>Please update the proxy to version {{ proxyClient.VERSION }}</p>
<p>Run:</p>
<pre>python3 $ANDROID_BUILD_TOP/development/tools/winscope/adb_proxy/winscope_proxy.py</pre>
<p>Or get it from the AOSP repository.</p>
</div>
<div class="md-layout">
<md-button class="md-accent" :href="downloadProxyUrl">Download from AOSP</md-button>
<md-button class="md-accent" @click="restart">Retry</md-button>
</div>
</md-card-content>
<md-card-content v-if="status === STATES.UNAUTH">
<md-icon class="md-accent">lock</md-icon>
<span class="md-subheading">Proxy authorisation required</span>
<md-field>
<label>Enter Winscope proxy token</label>
<md-input v-model="proxyClient.store.proxyKey"></md-input>
</md-field>
<div class="md-body-2">The proxy token is printed to console on proxy launch, copy and paste it above.</div>
<div class="md-layout">
<md-button class="md-primary" @click="restart">Connect</md-button>
</div>
</md-card-content>
<md-card-content v-if="status === STATES.DEVICES">
<div class="md-subheading">{{ Object.keys(proxyClient.devices).length > 0 ? "Connected devices:" : "No devices detected" }}</div>
<md-list>
<md-list-item v-for="(device, id) in proxyClient.devices" :key="id" @click="proxyClient.selectDevice(id)" :disabled="!device.authorised">
<md-icon>{{ device.authorised ? "smartphone" : "screen_lock_portrait" }}</md-icon>
<span class="md-list-item-text">{{ device.authorised ? device.model : "unauthorised" }} ({{ id }})</span>
</md-list-item>
</md-list>
<md-progress-spinner :md-size="30" md-indeterminate></md-progress-spinner>
</md-card-content>
<md-card-content v-if="status === STATES.START_TRACE">
<div class="device-choice">
<md-list>
<md-list-item>
<md-icon>smartphone</md-icon>
<span class="md-list-item-text">{{ proxyClient.devices[proxyClient.selectedDevice].model }} ({{ proxyClient.selectedDevice }})</span>
</md-list-item>
</md-list>
<md-button class="md-primary" @click="resetLastDevice">Change device</md-button>
</div>
<div class="trace-section">
<h3>Trace targets:</h3>
<div class="selection">
<md-checkbox class="md-primary" v-for="traceKey in Object.keys(DYNAMIC_TRACES)" :key="traceKey" v-model="traceStore[traceKey]">{{ DYNAMIC_TRACES[traceKey].name }}</md-checkbox>
</div>
<div class="trace-config">
<h4>Surface Flinger config</h4>
<div class="selection">
<md-checkbox class="md-primary" v-for="config in TRACE_CONFIG['layers_trace']" :key="config" v-model="traceStore[config]">{{config}}</md-checkbox>
<div class="selection">
<md-field class="config-selection" v-for="selectConfig in Object.keys(SF_SELECTED_CONFIG)" :key="selectConfig">
<md-select v-model="SF_SELECTED_CONFIG_VALUES[selectConfig]" :placeholder="selectConfig">
<md-option value="">{{selectConfig}}</md-option>
<md-option v-for="option in SF_SELECTED_CONFIG[selectConfig]" :key="option" :value="option">{{ option }}</md-option>
</md-select>
</md-field>
</div>
</div>
</div>
<div class="trace-config">
<h4>Window Manager config</h4>
<div class="selection">
<md-field class="config-selection" v-for="selectConfig in Object.keys(WM_SELECTED_CONFIG)" :key="selectConfig">
<md-select v-model="WM_SELECTED_CONFIG_VALUES[selectConfig]" :placeholder="selectConfig">
<md-option value="">{{selectConfig}}</md-option>
<md-option v-for="option in WM_SELECTED_CONFIG[selectConfig]" :key="option" :value="option">{{ option }}</md-option>
</md-select>
</md-field>
</div>
</div>
<md-button class="md-primary trace-btn" @click="startTrace">Start trace</md-button>
</div>
<div class="dump-section">
<h3>Dump targets:</h3>
<div class="selection">
<md-checkbox class="md-primary" v-for="dumpKey in Object.keys(DUMPS)" :key="dumpKey" v-model="traceStore[dumpKey]">{{DUMPS[dumpKey].name}}</md-checkbox>
</div>
<div class="md-layout">
<md-button class="md-primary dump-btn" @click="dumpState">Dump state</md-button>
</div>
</div>
</md-card-content>
<md-card-content v-if="status === STATES.ERROR">
<md-icon class="md-accent">error</md-icon>
<span class="md-subheading">Error:</span>
<pre>
{{ errorText }}
</pre>
<md-button class="md-primary" @click="restart">Retry</md-button>
</md-card-content>
<md-card-content v-if="status === STATES.END_TRACE">
<span class="md-subheading">Tracing...</span>
<md-progress-bar md-mode="indeterminate"></md-progress-bar>
<div class="md-layout">
<md-button class="md-primary" @click="endTrace">End trace</md-button>
</div>
</md-card-content>
<md-card-content v-if="status === STATES.LOAD_DATA">
<span class="md-subheading">Loading data...</span>
<md-progress-bar md-mode="determinate" :md-value="loadProgress"></md-progress-bar>
</md-card-content>
</flat-card>
</template>
<script>
import LocalStore from './localstore.js';
import FlatCard from './components/FlatCard.vue';
import {proxyClient, ProxyState, ProxyEndpoint} from './proxyclient/ProxyClient.ts';
// trace options should be added in a nested category
const TRACES = {
'default': {
'window_trace': {
name: 'Window Manager',
},
'accessibility_trace': {
name: 'Accessibility',
},
'layers_trace': {
name: 'Surface Flinger',
},
'transactions': {
name: 'Transaction',
},
'proto_log': {
name: 'ProtoLog',
},
'screen_recording': {
name: 'Screen Recording',
},
'ime_trace_clients': {
name: 'Input Method Clients',
},
'ime_trace_service': {
name: 'Input Method Service',
},
'ime_trace_managerservice': {
name: 'Input Method Manager Service',
},
},
'arc': {
'wayland_trace': {
name: 'Wayland',
},
},
};
const TRACE_CONFIG = {
'layers_trace': [
'composition',
'metadata',
'hwc',
'tracebuffers',
],
};
const SF_SELECTED_CONFIG = {
'sfbuffersize': [
'4000',
'8000',
'16000',
'32000',
],
};
const WM_SELECTED_CONFIG = {
'wmbuffersize': [
'4000',
'8000',
'16000',
'32000',
],
'tracingtype': [
'frame',
'transaction',
],
'tracinglevel': [
'verbose',
'debug',
'critical',
],
};
const DUMPS = {
'window_dump': {
name: 'Window Manager',
},
'layers_dump': {
name: 'Surface Flinger',
},
};
const CONFIGS = Object.keys(TRACE_CONFIG).flatMap((file) => TRACE_CONFIG[file]);
export default {
name: 'dataadb',
data() {
return {
proxyClient,
ProxyState,
STATES: ProxyState,
TRACES,
DYNAMIC_TRACES: TRACES['default'],
TRACE_CONFIG,
SF_SELECTED_CONFIG,
WM_SELECTED_CONFIG,
SF_SELECTED_CONFIG_VALUES: {},
WM_SELECTED_CONFIG_VALUES: {},
DUMPS,
status: ProxyState.CONNECTING,
dataFiles: [],
keep_alive_worker: null,
errorText: '',
loadProgress: 0,
traceStore: LocalStore(
'trace',
Object.assign(
this.getAllTraceKeys(TRACES)
.concat(Object.keys(DUMPS))
.concat(CONFIGS)
.reduce(function(obj, key) {
obj[key] = true; return obj;
}, {}),
),
),
downloadProxyUrl: 'https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py',
onStateChangeFn: (newState, errorText) => {
this.status = newState;
this.errorText = errorText;
},
};
},
props: ['store'],
components: {
'flat-card': FlatCard,
},
methods: {
getAllTraceKeys(traces) {
let keys = [];
for (let dict_key in traces) {
for (let key in traces[dict_key]) {
keys.push(key);
}
}
return keys;
},
setAvailableTraces() {
this.DYNAMIC_TRACES = this.TRACES['default'];
proxyClient.call('GET', ProxyEndpoint.CHECK_WAYLAND, this, function(request, view) {
try {
if(request.responseText == 'true') {
view.appendOptionalTraces('arc');
}
} catch(err) {
console.error(err);
proxyClient.setState(ProxyState.ERROR, request.responseText);
}
});
},
appendOptionalTraces(device_key) {
for(let key in this.TRACES[device_key]) {
this.$set(this.DYNAMIC_TRACES, key, this.TRACES[device_key][key]);
}
},
keepAliveTrace() {
if (this.status !== ProxyState.END_TRACE) {
clearInterval(this.keep_alive_worker);
this.keep_alive_worker = null;
return;
}
proxyClient.call('GET', `${ProxyEndpoint.STATUS}${proxyClient.deviceId()}/`, this, function(request, view) {
if (request.responseText !== 'True') {
view.endTrace();
} else if (view.keep_alive_worker === null) {
view.keep_alive_worker = setInterval(view.keepAliveTrace, 1000);
}
});
},
startTrace() {
const requested = this.toTrace();
const requestedConfig = this.toTraceConfig();
const requestedSelectedSfConfig = this.toSelectedSfTraceConfig();
const requestedSelectedWmConfig = this.toSelectedWmTraceConfig();
if (requested.length < 1) {
proxyClient.setState(ProxyState.ERROR, 'No targets selected');
this.recordNewEvent("No targets selected");
return;
}
this.recordNewEvent("Start Trace");
proxyClient.call('POST', `${ProxyEndpoint.CONFIG_TRACE}${proxyClient.deviceId()}/`, this, null, null, requestedConfig);
proxyClient.call('POST', `${ProxyEndpoint.SELECTED_SF_CONFIG_TRACE}${proxyClient.deviceId()}/`, this, null, null, requestedSelectedSfConfig);
proxyClient.call('POST', `${ProxyEndpoint.SELECTED_WM_CONFIG_TRACE}${proxyClient.deviceId()}/`, this, null, null, requestedSelectedWmConfig);
proxyClient.setState(ProxyState.END_TRACE);
proxyClient.call('POST', `${ProxyEndpoint.START_TRACE}${proxyClient.deviceId()}/`, this, function(request, view) {
view.keepAliveTrace();
}, null, requested);
},
dumpState() {
this.recordButtonClickedEvent("Dump State");
const requested = this.toDump();
if (requested.length < 1) {
proxyClient.setState(ProxyState.ERROR, 'No targets selected');
this.recordNewEvent("No targets selected");
return;
}
proxyClient.setState(ProxyState.LOAD_DATA);
proxyClient.call('POST', `${ProxyEndpoint.DUMP}${proxyClient.deviceId()}/`, this, function(request, view) {
proxyClient.loadFile(requested, 0, "dump", view);
}, null, requested);
},
endTrace() {
proxyClient.setState(ProxyState.LOAD_DATA);
proxyClient.call('POST', `${ProxyEndpoint.END_TRACE}${proxyClient.deviceId()}/`, this, function(request, view) {
proxyClient.loadFile(view.toTrace(), 0, "trace", view);
});
this.recordNewEvent("Ended Trace");
},
toTrace() {
return Object.keys(this.DYNAMIC_TRACES)
.filter((traceKey) => this.traceStore[traceKey]);
},
toTraceConfig() {
return Object.keys(TRACE_CONFIG)
.filter((file) => this.traceStore[file])
.flatMap((file) => TRACE_CONFIG[file])
.filter((config) => this.traceStore[config]);
},
toSelectedSfTraceConfig() {
const requestedSelectedConfig = {};
for (const config in this.SF_SELECTED_CONFIG_VALUES) {
if (this.SF_SELECTED_CONFIG_VALUES[config] !== "") {
requestedSelectedConfig[config] = this.SF_SELECTED_CONFIG_VALUES[config];
}
}
return requestedSelectedConfig;
},
toSelectedWmTraceConfig() {
const requestedSelectedConfig = {};
for (const config in this.WM_SELECTED_CONFIG_VALUES) {
if (this.WM_SELECTED_CONFIG_VALUES[config] !== "") {
requestedSelectedConfig[config] = this.WM_SELECTED_CONFIG_VALUES[config];
}
}
return requestedSelectedConfig;
},
toDump() {
return Object.keys(DUMPS)
.filter((dumpKey) => this.traceStore[dumpKey]);
},
restart() {
this.recordButtonClickedEvent("Connect / Retry");
proxyClient.setState(ProxyState.CONNECTING);
},
resetLastDevice() {
this.recordButtonClickedEvent("Change Device");
this.proxyClient.resetLastDevice();
this.restart();
},
},
created() {
proxyClient.setState(ProxyState.CONNECTING);
this.proxyClient.onStateChange(this.onStateChangeFn);
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('token')) {
this.proxyClient.proxyKey = urlParams.get('token');
}
this.proxyClient.getDevices();
},
beforeDestroy() {
this.proxyClient.removeOnStateChange(this.onStateChangeFn);
},
watch: {
status: {
handler(st) {
if (st == ProxyState.CONNECTING) {
this.proxyClient.getDevices();
}
if (st == ProxyState.START_TRACE) {
this.setAvailableTraces();
}
},
},
},
};
</script>
<style scoped>
.config-selection {
width: 150px;
display: inline-flex;
margin-left: 5px;
margin-right: 5px;
}
.device-choice {
display: inline-flex;
}
h3 {
margin-bottom: 0;
}
.trace-btn, .dump-btn {
margin-top: 0;
}
pre {
white-space: pre-wrap;
}
</style>

View File

@@ -1,700 +0,0 @@
<!-- Copyright (C) 2019 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.
-->
<template>
<div @dragleave="fileDragOut" @dragover="fileDragIn" @drop="handleFileDrop">
<flat-card style="min-width: 50em">
<md-card-header>
<div class="md-title">Open files</div>
</md-card-header>
<md-card-content>
<div class="dropbox" @click="$refs.fileUpload.click()" ref="dropbox">
<md-list
class="uploaded-files"
v-show="Object.keys(dataFiles).length > 0"
>
<md-list-item v-for="file in dataFiles" v-bind:key="file.filename">
<md-icon>{{FILE_ICONS[file.type]}}</md-icon>
<span class="md-list-item-text">{{file.filename}} ({{file.type}})
</span>
<md-button
class="md-icon-button md-accent"
@click="e => {
e.stopPropagation()
onRemoveFile(file.type)
}"
>
<md-icon>close</md-icon>
</md-button>
</md-list-item>
</md-list>
<div class="progress-spinner-wrapper" v-show="loadingFiles">
<md-progress-spinner
:md-diameter="30"
:md-stroke="3"
md-mode="indeterminate"
class="progress-spinner"
/>
</div>
<input
type="file"
@change="onLoadFile"
v-on:drop="handleFileDrop"
ref="fileUpload"
id="dropzone"
v-show="false"
multiple
/>
<p v-if="!dataReady && !loadingFiles">
Drag your <b>.winscope</b> or <b>.zip</b> file(s) or click here to begin
</p>
</div>
<div class="md-layout">
<div class="md-layout-item md-small-size-100">
<md-field>
<md-select v-model="fileType" id="file-type" placeholder="File type">
<md-option value="auto">Detect type</md-option>
<md-option value="bugreport">Bug Report (.zip)</md-option>
<md-option
:value="k" v-for="(v,k) in FILE_DECODERS"
v-bind:key="v.name">{{v.name}}
></md-option>
</md-select>
</md-field>
</div>
</div>
<div class="md-layout">
<md-button
class="md-primary md-theme-default"
@click="$refs.fileUpload.click()"
>
Add File
</md-button>
<md-button
v-if="dataReady"
@click="onSubmit"
class="md-button md-primary md-raised md-theme-default"
>
Submit
</md-button>
</div>
</md-card-content>
<md-snackbar
md-position="center"
:md-duration="Infinity"
:md-active.sync="showFetchingSnackbar"
md-persistent
>
<span>{{ fetchingSnackbarText }}</span>
</md-snackbar>
<md-snackbar
md-position="center"
:md-duration="snackbarDuration"
:md-active.sync="showSnackbar"
md-persistent
>
<p class="snackbar-break-words">{{ snackbarText }}</p>
<div @click="hideSnackbarMessage()">
<md-button class="md-icon-button">
<md-icon style="color: white">close</md-icon>
</md-button>
</div>
</md-snackbar>
</flat-card>
</div>
</template>
<script>
import FlatCard from './components/FlatCard.vue';
import JSZip from 'jszip';
import {
detectAndDecode,
FILE_TYPES,
FILE_DECODERS,
FILE_ICONS,
UndetectableFileType,
} from './decode.js';
import {WebContentScriptMessageType} from './utils/consts';
import {combineWmSfWithImeDataIfExisting} from './ime_processing.js';
export default {
name: 'datainput',
data() {
return {
FILE_TYPES,
FILE_DECODERS,
FILE_ICONS,
fileType: 'auto',
dataFiles: {},
loadingFiles: false,
showFetchingSnackbar: false,
showSnackbar: false,
snackbarDuration: 3500,
snackbarText: '',
fetchingSnackbarText: 'Fetching files...',
traceName: undefined,
};
},
props: ['store'],
created() {
// Attempt to load files from extension if present
this.loadFilesFromExtension();
},
mounted() {
this.handleDropboxDragEvents();
},
beforeUnmount() {
},
methods: {
showSnackbarMessage(message, duration) {
this.snackbarText = '\n' + message + '\n';
this.snackbarDuration = duration;
this.showSnackbar = true;
},
hideSnackbarMessage() {
this.showSnackbar = false;
this.recordButtonClickedEvent("Hide Snackbar Message")
},
getFetchFilesLoadingAnimation() {
let frame = 0;
const fetchingStatusAnimation = () => {
frame++;
this.fetchingSnackbarText = `Fetching files${'.'.repeat(frame % 4)}`;
};
let interval = undefined;
return Object.freeze({
start: () => {
this.showFetchingSnackbar = true;
interval = setInterval(fetchingStatusAnimation, 500);
},
stop: () => {
this.showFetchingSnackbar = false;
clearInterval(interval);
},
});
},
handleDropboxDragEvents() {
// Counter used to keep track of when we actually exit the dropbox area
// When we drag over a child of the dropbox area the dragenter event will
// be called again and subsequently the dragleave so we don't want to just
// remove the class on the dragleave event.
let dropboxDragCounter = 0;
console.log(this.$refs["dropbox"])
this.$refs["dropbox"].addEventListener('dragenter', e => {
dropboxDragCounter++;
this.$refs["dropbox"].classList.add('dragover');
});
this.$refs["dropbox"].addEventListener('dragleave', e => {
dropboxDragCounter--;
if (dropboxDragCounter == 0) {
this.$refs["dropbox"].classList.remove('dragover');
}
});
this.$refs["dropbox"].addEventListener('drop', e => {
dropboxDragCounter = 0;
this.$refs["dropbox"].classList.remove('dragover');
});
},
/**
* Attempt to load files from the extension if present.
*
* If the source URL parameter is set to the extension it make a request
* to the extension to fetch the files from the extension.
*/
loadFilesFromExtension() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('source') === 'openFromExtension' && chrome) {
// Fetch files from extension
const androidBugToolExtensionId = 'mbbaofdfoekifkfpgehgffcpagbbjkmj';
const loading = this.getFetchFilesLoadingAnimation();
loading.start();
// Request to convert the blob object url "blob:chrome-extension://xxx"
// the chrome extension has to a web downloadable url "blob:http://xxx".
chrome.runtime.sendMessage(androidBugToolExtensionId, {
action: WebContentScriptMessageType.CONVERT_OBJECT_URL,
}, async (response) => {
switch (response.action) {
case WebContentScriptMessageType.CONVERT_OBJECT_URL_RESPONSE:
if (response.attachments?.length > 0) {
const filesBlobPromises = response.attachments
.map(async (attachment) => {
const fileQueryResponse =
await fetch(attachment.objectUrl);
const blob = await fileQueryResponse.blob();
/**
* Note: The blob's media type is not correct.
* It is always set to "image/png".
* Context: http://google3/javascript/closure/html/safeurl.js?g=0&l=256&rcl=273756987
*/
// Clone blob to clear media type.
const file = new Blob([blob]);
file.name = attachment.name;
return file;
});
const files = await Promise.all(filesBlobPromises);
loading.stop();
this.processFiles(files);
} else {
const failureMessages = 'Got no attachements from extension...';
console.warn(failureMessages);
this.showSnackbarMessage(failureMessages, 3500);
}
break;
default:
loading.stop();
const failureMessages =
'Received unhandled response code from extension.';
console.warn(failureMessages);
this.showSnackbarMessage(failureMessages, 3500);
}
});
}
},
fileDragIn(e) {
e.preventDefault();
},
fileDragOut(e) {
e.preventDefault();
},
handleFileDrop(e) {
e.preventDefault();
let droppedFiles = e.dataTransfer.files;
if(!droppedFiles) return;
// Record analytics event
this.recordDragAndDropFileEvent(droppedFiles);
this.processFiles(droppedFiles);
},
onLoadFile(e) {
const files = event.target.files || event.dataTransfer.files;
this.recordFileUploadEvent(files);
this.processFiles(files);
},
async processFiles(files) {
console.log("Object.keys(this.dataFiles).length", Object.keys(this.dataFiles).length)
// The trace name to use if we manage to load the archive without errors.
let tmpTraceName;
if (Object.keys(this.dataFiles).length > 0) {
// We have already loaded some files so only want to use the name of
// this archive as the name of the trace if we override all loaded files
} else {
// No files have been uploaded yet so if we are uploading only 1 archive
// we want to use it's name as the trace name
if (files.length == 1 && this.isArchive(files[0])) {
tmpTraceName = this.getFileNameWithoutZipExtension(files[0])
}
}
let error;
const decodedFiles = [];
for (const file of files) {
try {
this.loadingFiles = true;
this.showSnackbarMessage(`Loading ${file.name}`, Infinity);
const result = await this.addFile(file);
decodedFiles.push(...result);
this.hideSnackbarMessage();
} catch (e) {
this.showSnackbarMessage(
`Failed to load '${file.name}'...\n${e}`, 5000);
console.error(e);
error = e;
break;
} finally {
this.loadingFiles = false;
}
}
event.target.value = '';
if (error) {
return;
}
// TODO: Handle the fact that we can now have multiple files of type
// FILE_TYPES.TRANSACTION_EVENTS_TRACE
const decodedFileTypes = new Set(Object.keys(this.dataFiles));
// A file is overridden if a file of the same type is upload twice, as
// Winscope currently only support at most one file to each type
const overriddenFileTypes = new Set();
const overriddenFiles = {}; // filetype => array of files
for (const decodedFile of decodedFiles) {
const dataType = decodedFile.filetype;
if (decodedFileTypes.has(dataType)) {
overriddenFileTypes.add(dataType);
(overriddenFiles[dataType] = overriddenFiles[dataType] || [])
.push(this.dataFiles[dataType]);
}
decodedFileTypes.add(dataType);
const frozenData = Object.freeze(decodedFile.data.data);
delete decodedFile.data.data;
decodedFile.data.data = frozenData;
this.$set(this.dataFiles,
dataType, Object.freeze(decodedFile.data));
}
// TODO(b/169305853): Remove this once we have magic numbers or another
// way to detect the file type more reliably.
for (const dataType in overriddenFiles) {
if (overriddenFiles.hasOwnProperty(dataType)) {
const files = overriddenFiles[dataType];
files.push(this.dataFiles[dataType]);
const selectedFile =
this.getMostLikelyCandidateFile(dataType, files);
this.$set(this.dataFiles, dataType, Object.freeze(selectedFile));
// Remove selected file from overriden list
const index = files.indexOf(selectedFile);
files.splice(index, 1);
}
}
if (overriddenFileTypes.size > 0) {
this.displayFilesOverridenWarning(overriddenFiles);
}
if (tmpTraceName !== undefined) {
this.traceName = tmpTraceName;
}
if (this.store.betaFeatures.newImePanels) {
combineWmSfWithImeDataIfExisting(this.dataFiles);
}
},
getFileNameWithoutZipExtension(file) {
const fileNameSplitOnDot = file.name.split('.')
if (fileNameSplitOnDot.slice(-1)[0] == 'zip') {
return fileNameSplitOnDot.slice(0,-1).join('.');
} else {
return file.name;
}
},
/**
* Gets the file that is most likely to be the actual file of that type out
* of all the candidateFiles. This is required because there are some file
* types that have no magic number and may lead to false positives when
* decoding in decode.js. (b/169305853)
* @param {string} dataType - The type of the candidate files.
* @param {files[]} candidateFiles - The list all the files detected to be
* of type dataType, passed in the order
* they are detected/uploaded in.
* @return {file} - the most likely candidate.
*/
getMostLikelyCandidateFile(dataType, candidateFiles) {
const keyWordsByDataType = {
[FILE_TYPES.WINDOW_MANAGER_DUMP]: 'window',
[FILE_TYPES.SURFACE_FLINGER_DUMP]: 'surface',
};
if (
!candidateFiles ||
!candidateFiles.length ||
candidateFiles.length == 0
) {
throw new Error('No candidate files provided');
}
if (!keyWordsByDataType.hasOwnProperty(dataType)) {
console.warn(`setMostLikelyCandidateFile doesn't know how to handle ` +
`candidates of dataType ${dataType} setting last candidate as ` +
`target file.`);
// We want to return the last candidate file so that, we always override
// old uploaded files with once of the latest uploaded files.
return candidateFiles.slice(-1)[0];
}
for (const file of candidateFiles) {
if (file.filename
.toLowerCase().includes(keyWordsByDataType[dataType])) {
return file;
}
}
// We want to return the last candidate file so that, we always override
// old uploaded files with once of the latest uploaded files.
return candidateFiles.slice(-1)[0];
},
/**
* Display a snackbar warning that files have been overriden and any
* relavant additional information in the logs.
* @param {{string: file[]}} overriddenFiles - a mapping from data types to
* the files of the of that datatype tha have been overriden.
*/
displayFilesOverridenWarning(overriddenFiles) {
const overriddenFileTypes = Object.keys(overriddenFiles);
const overriddenCount = Object.values(overriddenFiles)
.map((files) => files.length).reduce((length, next) => length + next);
if (overriddenFileTypes.length === 1 && overriddenCount === 1) {
const type = overriddenFileTypes.values().next().value;
const overriddenFile = overriddenFiles[type][0].filename;
const keptFile = this.dataFiles[type].filename;
const message =
`'${overriddenFile}' is conflicting with '${keptFile}'. ` +
`Only '${keptFile}' will be kept. If you wish to display ` +
`'${overriddenFile}', please upload it again with no other file ` +
`of the same type.`;
this.showSnackbarMessage(`WARNING: ${message}`, Infinity);
console.warn(message);
} else {
const message = `Mutiple conflicting files have been uploaded. ` +
`${overriddenCount} files have been discarded. Please check the ` +
`developer console for more information.`;
this.showSnackbarMessage(`WARNING: ${message}`, Infinity);
const messageBuilder = [];
for (const type of overriddenFileTypes.values()) {
const keptFile = this.dataFiles[type].filename;
const overriddenFilesCount = overriddenFiles[type].length;
messageBuilder.push(`${overriddenFilesCount} file` +
`${overriddenFilesCount > 1 ? 's' : ''} of type ${type} ` +
`${overriddenFilesCount > 1 ? 'have' : 'has'} been ` +
`overridden. Only '${keptFile}' has been kept.`);
}
messageBuilder.push('');
messageBuilder.push('Please reupload the specific files you want ' +
'to read (one of each type).');
messageBuilder.push('');
messageBuilder.push('===============DISCARDED FILES===============');
for (const type of overriddenFileTypes.values()) {
const discardedFiles = overriddenFiles[type];
messageBuilder.push(`The following files of type ${type} ` +
`have been discarded:`);
for (const discardedFile of discardedFiles) {
messageBuilder.push(` - ${discardedFile.filename}`);
}
messageBuilder.push('');
}
console.warn(messageBuilder.join('\n'));
}
},
getFileExtensions(file) {
const split = file.name.split('.');
if (split.length > 1) {
return split.pop();
}
return undefined;
},
isArchive(file) {
const type = this.fileType;
const extension = this.getFileExtensions(file);
// extension === 'zip' is required on top of file.type ===
// 'application/zip' because when loaded from the extension the type is
// incorrect. See comment in loadFilesFromExtension() for more
// information.
return type === 'bugreport' ||
(type === 'auto' && (extension === 'zip' ||
file.type === 'application/zip'))
},
async addFile(file) {
const decodedFiles = [];
if (this.isArchive(file)) {
const results = await this.decodeArchive(file);
decodedFiles.push(...results);
} else {
const decodedFile = await this.decodeFile(file);
decodedFiles.push(decodedFile);
}
return decodedFiles;
},
readFile(file) {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onload = async (e) => {
const buffer = new Uint8Array(e.target.result);
resolve(buffer);
};
reader.readAsArrayBuffer(file);
});
},
async decodeFile(file) {
const buffer = await this.readFile(file);
let filetype = this.filetype;
let data;
if (filetype) {
const fileDecoder = FILE_DECODERS[filetype];
data = fileDecoder.decoder(
buffer, fileDecoder.decoderParams, file.name, this.store);
} else {
// Defaulting to auto — will attempt to detect file type
[filetype, data] = detectAndDecode(buffer, file.name, this.store);
}
return {filetype, data};
},
/**
* Decode a zip file
*
* Load all files that can be decoded, even if some failures occur.
* For example, a zip file with an mp4 recorded via MediaProjection
* doesn't include the winscope metadata (b/140855415), but the trace
* files within the zip should be nevertheless readable
*/
async decodeArchive(archive) {
const buffer = await this.readFile(archive);
const zip = new JSZip();
const content = await zip.loadAsync(buffer);
const decodedFiles = [];
let lastError;
for (const filename in content.files) {
const file = content.files[filename];
if (file.dir) {
// Ignore directories
continue;
}
const fileBlob = await file.async('blob');
// Get only filename and remove rest of path
fileBlob.name = filename.split('/').slice(-1).pop();
try {
const decodedFile = await this.decodeFile(fileBlob);
decodedFiles.push(decodedFile);
} catch (e) {
if (!(e instanceof UndetectableFileType)) {
lastError = e;
}
console.error(e);
}
}
if (decodedFiles.length == 0) {
if (lastError) {
throw lastError;
}
throw new Error('No matching files found in archive', archive);
} else {
if (lastError) {
this.showSnackbarMessage(
'Unable to parse all files, check log for more details', 3500);
}
}
return decodedFiles;
},
onRemoveFile(typeName) {
this.$delete(this.dataFiles, typeName);
},
onSubmit() {
this.$emit('dataReady', this.formattedTraceName,
Object.keys(this.dataFiles).map((key) => this.dataFiles[key]));
},
},
computed: {
dataReady: function() {
return Object.keys(this.dataFiles).length > 0;
},
formattedTraceName() {
if (this.traceName === undefined) {
return 'winscope-trace';
} else {
return this.traceName;
}
}
},
components: {
'flat-card': FlatCard,
},
};
</script>
<style>
.dropbox:hover, .dropbox.dragover {
background: rgb(224, 224, 224);
}
.dropbox {
outline: 2px dashed #448aff; /* the dash box */
outline-offset: -10px;
background: white;
color: #448aff;
padding: 10px 10px 10px 10px;
min-height: 200px; /* minimum height */
position: relative;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-items: center;
}
.dropbox p, .dropbox .progress-spinner-wrapper {
font-size: 1.2em;
margin: auto;
}
.progress-spinner-wrapper, .progress-spinner {
width: fit-content;
height: fit-content;
display: block;
}
.progress-spinner-wrapper {
padding: 1.5rem 0 1.5rem 0;
}
.dropbox .uploaded-files {
background: none!important;
width: 100%;
}
</style>

View File

@@ -1,245 +0,0 @@
<!-- Copyright (C) 2019 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.
-->
<template>
<div @click="onClick($event)">
<flat-card v-if="hasDataView(file)">
<md-card-header>
<md-card-header-text>
<div class="md-title">
<button class="toggle-view-button" @click="toggleView">
<i aria-hidden="true" class="md-icon md-theme-default material-icons">
{{ isShowFileType(file.type) ? "expand_more" : "chevron_right" }}
</i>
</button>
<md-icon>{{ TRACE_ICONS[file.type] }}</md-icon>
{{ file.type }}
</div>
</md-card-header-text>
<md-button
:href="file.blobUrl"
:download="file.type"
class="md-icon-button"
>
<md-icon>save_alt</md-icon>
</md-button>
<md-button @click="takeScreenshot" class="md-icon-button">
<md-icon>camera_alt</md-icon>
</md-button>
</md-card-header>
<AccessibilityTraceView
v-if="showInAccessibilityTraceView(file) && isShowFileType(file.type)"
:store="store"
:file="file"
ref="view"
/>
<WindowManagerTraceView
v-if="showInWindowManagerTraceView(file) && isShowFileType(file.type)"
:store="store"
:file="file"
:presentTags="presentTags"
:presentErrors="presentErrors"
ref="view"
/>
<SurfaceFlingerTraceView
v-else-if="showInSurfaceFlingerTraceView(file) && isShowFileType(file.type)"
:store="store"
:file="file"
:presentTags="presentTags"
:presentErrors="presentErrors"
ref="view"
/>
<ImeTraceView
v-else-if="showInImeTraceView(file) && isShowFileType(file.type)"
:store="store"
:file="file"
:presentTags="presentTags"
:presentErrors="presentErrors"
ref="view"
/>
<transactionsviewlegacy
v-else-if="isTransactionsLegacy(file) && isShowFileType(file.type)"
:trace="file"
ref="view"
/>
<logview
v-else-if="isLog(file) && isShowFileType(file.type)"
:file="file"
ref="view"
/>
<traceview
v-else-if="showInTraceView(file) && isShowFileType(file.type)"
:store="store"
:file="file"
:presentTags="[]"
:presentErrors="[]"
:propertyGroups="false"
ref="view"
/>
<div v-else>
<h1 v-if="isShowFileType(file.type)" class="bad">Unrecognized DataType</h1>
</div>
</flat-card>
</div>
</template>
<script>
import TraceView from '@/TraceView.vue';
import AccessibilityTraceView from '@/AccessibilityTraceView.vue';
import WindowManagerTraceView from '@/WindowManagerTraceView.vue';
import SurfaceFlingerTraceView from '@/SurfaceFlingerTraceView.vue';
import ImeTraceView from '@/ImeTraceView';
import TransactionsViewLegacy from '@/TransactionsViewLegacy.vue';
import LogView from '@/LogView.vue';
import FileType from '@/mixins/FileType.js';
import FlatCard from '@/components/FlatCard.vue';
import {TRACE_ICONS} from '@/decode.js';
import html2canvas from 'html2canvas';
export default {
name: 'dataview',
data() {
return {
TRACE_ICONS,
};
},
methods: {
// Recursively search for an arrowUp method in the children
// This is necessary because the VueComponent hierarchy has
// different depths depending on the source
depthFirstSearchArrowUp(component) {
if (component.arrowUp) {
component.arrowUp();
return true;
} else {
for (let i = 0; i < component.$children.length; i++) {
const child = component.$children[i];
if (this.depthFirstSearchArrowUp(child)) {
return true;
}
}
return false;
}
},
// Recursively search for an arrowUp method in the children
// This is necessary because the VueComponent hierarchy has
// different depths depending on the source
depthFirstSearchArrowDown(component) {
if (component.arrowDown) {
component.arrowDown();
return true;
} else {
for (let i = 0; i < component.$children.length; i++) {
const child = component.$children[i];
if (this.depthFirstSearchArrowDown(child)) {
return true;
}
}
return false;
}
},
arrowUp() {
for (let i = 0; i < this.$children.length; i++) {
const child = this.$children[i];
const done = this.depthFirstSearchArrowUp(child);
if (done) {
return true;
}
}
return false;
},
arrowDown() {
for (let i = 0; i < this.$children.length; i++) {
const child = this.$children[i];
const done = this.depthFirstSearchArrowDown(child);
if (done) {
return true;
}
}
return false;
},
onClick(e) {
// Pass click event to parent, so that click event handler can be attached
// to component.
this.$emit('click', e);
},
/** Filter data view files by current show settings */
updateShowFileTypes() {
this.store.showFileTypes = this.dataViewFiles
.filter((file) => file.show)
.map((file) => file.type);
},
/** Expand or collapse data view */
toggleView() {
this.file.show = !this.file.show;
this.updateShowFileTypes();
},
/** Check if data view file should be shown */
isShowFileType(type) {
return this.store.showFileTypes.find((fileType) => fileType===type);
},
takeScreenshot() {
html2canvas(this.$el)
.then((canvas) => {
const uri = canvas.toDataURL();
const filename = 'Winscope-' + this.file.type + '-Screenshot.png';
const link = document.createElement('a');
if (typeof link.download === 'string') {
link.href = uri;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
window.open(uri);
}
});
},
},
props: ['store', 'file', 'presentTags', 'presentErrors', 'dataViewFiles'],
mixins: [FileType],
components: {
'traceview': TraceView,
'transactionsviewlegacy': TransactionsViewLegacy,
'logview': LogView,
'flat-card': FlatCard,
AccessibilityTraceView,
WindowManagerTraceView,
SurfaceFlingerTraceView,
ImeTraceView,
},
};
</script>
<style>
.bad {
margin: 1em 1em 1em 1em;
font-size: 4em;
color: red;
}
.toggle-view-button {
background: none;
color: inherit;
border: none;
font: inherit;
cursor: pointer;
padding-right: 10px;
display: inline-block;
}
.md-title {
display: inline-block;
}
</style>

View File

@@ -1,141 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<span>
<span class="kind">{{item.kind}}</span>
<span v-if="item.kind && item.name">-</span>
<span
v-if="simplifyNames && item.shortName &&
item.shortName !== item.name"
>{{ item.shortName }} <!-- No line break on purpose -->
<md-tooltip
md-delay="300"
md-direction="top"
style="margin-bottom: -10px"
>
{{item.name}}
</md-tooltip>
</span>
<span v-else>{{ item.name }}</span>
<div
v-for="c in item.chips"
v-bind:key="c.long"
:title="c.long"
:class="chipClassForChip(c)"
>{{c.short}} <!-- No line break on purpose -->
<md-tooltip
md-delay="300"
md-direction="top"
style="margin-bottom: -10px"
>
{{c.long}}
</md-tooltip>
</div>
<div class="flicker-tags" v-for="transition in transitions" :key="transition">
<Arrow
class="transition-arrow"
:style="{color: transitionArrowColor(transition)}"
/>
<md-tooltip md-direction="right"> {{transitionTooltip(transition)}} </md-tooltip>
</div>
<div class="flicker-tags" v-for="error in errors" :key="error.message">
<Arrow class="error-arrow"/>
<md-tooltip md-direction="right"> {{errorTooltip(error.message)}} </md-tooltip>
</div>
</span>
</template>
<script>
import Arrow from './components/TagDisplay/Arrow.vue';
import {transitionMap} from './utils/consts.js';
export default {
name: 'DefaultTreeElement',
props: ['item', 'simplify-names', 'errors', 'transitions'],
methods: {
chipClassForChip(c) {
return [
'tree-view-internal-chip',
'tree-view-chip',
'tree-view-chip' + '-' +
(c.type?.toString() || c.class?.toString() || 'default'),
];
},
transitionArrowColor(transition) {
return transitionMap.get(transition).color;
},
transitionTooltip(transition) {
return transitionMap.get(transition).desc;
},
errorTooltip(errorMessage) {
if (errorMessage.length>100) {
return `Error: ${errorMessage.substring(0,100)}...`;
}
return `Error: ${errorMessage}`;
},
},
components: {
Arrow,
},
};
</script>
<style scoped>
.tree-view-internal-chip {
display: inline-block;
}
.tree-view-chip {
padding: 0 10px;
border-radius: 10px;
background-color: #aaa;
color: black;
}
.tree-view-chip.tree-view-chip-warn {
background-color: #ffaa6b;
color: black;
}
.tree-view-chip.tree-view-chip-error {
background-color: #ff6b6b;
color: black;
}
.tree-view-chip.tree-view-chip-gpu {
background-color: #00c853;
color: black;
}
.tree-view-chip.tree-view-chip-hwc {
background-color: #448aff;
color: black;
}
span {
overflow-wrap: break-word;
flex: 1 1 auto;
width: 0;
}
.flicker-tags {
display: inline-block;
}
.error-arrow {
color: red;
}
</style>

View File

@@ -1,229 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<div
class="draggable-container"
:style="{visibility: contentIsLoaded ? 'visible' : 'hidden'}"
>
<md-card class="draggable-card">
<div class="header" @mousedown="onHeaderMouseDown">
<md-icon class="drag-icon">
drag_indicator
</md-icon>
<slot name="header" />
</div>
<div class="content">
<slot name="main" ref="content"/>
<div class="resizer" v-show="resizeable" @mousedown="onResizerMouseDown"/>
</div>
</md-card>
</div>
</template>
<script>
export default {
name: "DraggableDiv",
// If asyncLoad is enabled must call contentLoaded when content is ready
props: ['position', 'asyncLoad', 'resizeable'],
data() {
return {
positions: {
clientX: undefined,
clientY: undefined,
movementX: 0,
movementY: 0,
},
parentResizeObserver: null,
contentIsLoaded: false,
extraWidth: 0,
extraHeight: 0,
}
},
methods: {
onHeaderMouseDown(e) {
e.preventDefault();
this.initDragAction(e);
},
onResizerMouseDown(e) {
e.preventDefault();
this.startResize(e);
},
initDragAction(e) {
this.positions.clientX = e.clientX;
this.positions.clientY = e.clientY;
document.onmousemove = this.startDrag;
document.onmouseup = this.stopDrag;
},
startDrag(e) {
e.preventDefault();
this.positions.movementX = this.positions.clientX - e.clientX;
this.positions.movementY = this.positions.clientY - e.clientY;
this.positions.clientX = e.clientX;
this.positions.clientY = e.clientY;
const parentHeight = this.$el.parentElement.clientHeight;
const parentWidth = this.$el.parentElement.clientWidth;
const divHeight = this.$el.clientHeight;
const divWidth = this.$el.clientWidth;
let top = this.$el.offsetTop - this.positions.movementY;
if (top < 0) {
top = 0;
}
if (top + divHeight > parentHeight) {
top = parentHeight - divHeight;
}
let left = this.$el.offsetLeft - this.positions.movementX;
if (left < 0) {
left = 0;
}
if (left + divWidth > parentWidth) {
left = parentWidth - divWidth;
}
this.$el.style.top = top + 'px';
this.$el.style.left = left + 'px';
},
stopDrag() {
document.onmouseup = null;
document.onmousemove = null;
},
startResize(e) {
e.preventDefault();
this.startResizeX = e.clientX;
this.startResizeY = e.clientY;
document.onmousemove = this.resizing;
document.onmouseup = this.stopResize;
document.body.style.cursor = "nwse-resize";
},
resizing(e) {
let extraWidth = this.extraWidth + (e.clientX - this.startResizeX);
if (extraWidth < 0) {
extraWidth = 0;
}
this.$emit('requestExtraWidth', extraWidth);
let extraHeight = this.extraHeight + (e.clientY - this.startResizeY);
if (extraHeight < 0) {
extraHeight = 0;
}
this.$emit('requestExtraHeight', extraHeight);
},
stopResize(e) {
this.extraWidth += e.clientX - this.startResizeX;
if (this.extraWidth < 0) {
this.extraWidth = 0;
}
this.extraHeight += e.clientY - this.startResizeY;
if (this.extraHeight < 0) {
this.extraHeight = 0;
}
document.onmousemove = null;
document.onmouseup = null;
document.body.style.cursor = null;
},
onParentResize() {
const parentHeight = this.$el.parentElement.clientHeight;
const parentWidth = this.$el.parentElement.clientWidth;
const elHeight = this.$el.clientHeight;
const elWidth = this.$el.clientWidth;
const rect = this.$el.getBoundingClientRect();
const offsetBottom = parentHeight - (rect.y + elHeight);
if (offsetBottom < 0) {
this.$el.style.top = parseInt(this.$el.style.top) + offsetBottom + 'px';
}
const offsetRight = parentWidth - (rect.x + elWidth);
if (offsetRight < 0) {
this.$el.style.left = parseInt(this.$el.style.left) + offsetRight + 'px';
}
},
contentLoaded() {
// To be called if content is loaded async (eg: video), so that div may
// position itself correctly.
if (this.contentIsLoaded) {
return;
}
this.contentIsLoaded = true;
const margin = 15;
switch (this.position) {
case 'bottomLeft':
this.moveToBottomLeft(margin);
break;
default:
throw new Error('Unsupported starting position for DraggableDiv');
}
},
moveToBottomLeft(margin) {
margin = margin || 0;
const divHeight = this.$el.clientHeight;
const parentHeight = this.$el.parentElement.clientHeight;
this.$el.style.top = parentHeight - divHeight - margin + 'px';
this.$el.style.left = margin + 'px';
},
},
mounted() {
if (!this.asyncLoad) {
this.contentLoaded();
}
// Listen for changes in parent height to avoid element exiting visible view
this.parentResizeObserver = new ResizeObserver(this.onParentResize);
this.parentResizeObserver.observe(this.$el.parentElement);
},
destroyed() {
this.parentResizeObserver.unobserve(this.$el.parentElement);
},
}
</script>
<style scoped>
.draggable-container {
position: absolute;
}
.draggable-card {
margin: 0;
}
.header {
cursor: grab;
padding: 3px;
}
.resizer {
position: absolute;
right: 0;
bottom: 0;
width: 0;
height: 0;
border-style: solid;
border-width: 0 0 15px 15px;
border-color: transparent transparent #ffffff transparent;
cursor: nwse-resize;
}
</style>

View File

@@ -1,455 +0,0 @@
<!-- 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.
-->
<template class="container">
<div v-if="isAllPropertiesNull" class="group">
There is no corresponding WM / SF entry for this IME entry
no WM / SF entry is recorded before this IME entry in time.
View later frames for WM & SF properties.
</div>
<div v-else-if="isImeManagerService">
<div class="group">
<button
class="text-button group-header"
v-if="wmProtoOrNull"
:class="{ 'selected': isSelected(wmProtoOrNull) }"
@click="onClickShowInPropertiesPanel(wmProtoOrNull)">
WMState
</button>
<span class="group-header" v-else>WMState</span>
<div class="full-width">
<span class="value" v-if="entry.wmProperties">{{
entry.wmProperties.name }}</span>
<span v-else>There is no corresponding WMState entry.</span>
</div>
</div>
<div class="group" v-if="wmInsetsSourceProviderOrNull">
<button
class="text-button group-header"
:class="{ 'selected': isSelected(wmInsetsSourceProviderOrNull) }"
@click="onClickShowInPropertiesPanel(wmInsetsSourceProviderOrNull)">
IME Insets Source Provider
</button>
<div class="full-width">
<div />
<span class="key">Source Frame:</span>
<CoordinatesTable
:coordinates="wmInsetsSourceProviderSourceFrameOrNull" />
<div />
<span class="key">Source Visible:</span>
<span class="value">{{
wmInsetsSourceProviderSourceVisibleOrNull }}</span>
<div />
<span class="key">Source Visible Frame:</span>
<CoordinatesTable
:coordinates="wmInsetsSourceProviderSourceVisibleFrameOrNull" />
<div />
<span class="key">Position:</span>
<span class="value">{{ wmInsetsSourceProviderPositionOrNull }}</span>
<div />
<span class="key">IsLeashReadyForDispatching:</span>
<span class="value">{{
wmInsetsSourceProviderIsLeashReadyOrNull }}</span>
<div />
<span class="key">Controllable:</span>
<span class="value">{{
wmInsetsSourceProviderControllableOrNull }}</span>
<div />
</div>
</div>
<div class="group" v-if="wmImeControlTargetOrNull">
<button
class="text-button group-header"
:class="{ 'selected': isSelected(wmImeControlTargetOrNull) }"
@click="onClickShowInPropertiesPanel(wmImeControlTargetOrNull)">
IME Control Target
</button>
<div class="full-width">
<span class="key" v-if="wmImeControlTargetTitleOrNull">Title:</span>
<span class="value" v-if="wmImeControlTargetTitleOrNull">{{
wmImeControlTargetTitleOrNull }}</span>
</div>
</div>
<div class="group" v-if="wmImeInputTargetOrNull">
<button
class="text-button group-header"
:class="{ 'selected': isSelected(wmImeInputTargetOrNull) }"
@click="onClickShowInPropertiesPanel(wmImeInputTargetOrNull)">
IME Input Target
</button>
<div class="full-width">
<span class="key" v-if="wmImeInputTargetTitleOrNull">Title:</span>
<span class="value" v-if="wmImeInputTargetTitleOrNull">{{
wmImeInputTargetTitleOrNull }}</span>
</div>
</div>
<div class="group" v-if="wmImeLayeringTargetOrNull">
<button
class="text-button group-header"
:class="{ 'selected': isSelected(wmImeLayeringTargetOrNull) }"
@click="onClickShowInPropertiesPanel(wmImeLayeringTargetOrNull)">
IME Layering Target
</button>
<div class="full-width">
<span class="key" v-if="wmImeLayeringTargetTitleOrNull">Title:</span>
<span class="value" v-if="wmImeLayeringTargetTitleOrNull">{{
wmImeLayeringTargetTitleOrNull }}</span>
</div>
</div>
</div>
<div v-else>
<!-- Ime Client or Ime Service -->
<div class="group">
<button
class="text-button group-header"
v-if="wmProtoOrNull"
:class="{ 'selected': isSelected(wmProtoOrNull) }"
@click="onClickShowInPropertiesPanel(wmProtoOrNull)">
WMState
</button>
<span class="group-header" v-else>WMState</span>
<div class="full-width">
<span class="value" v-if="entry.wmProperties">{{
entry.wmProperties.name }}</span>
<span v-else>There is no corresponding WMState entry.</span>
</div>
</div>
<div class="group">
<span class="group-header">SFLayer</span>
<div class="full-width">
<span class="value" v-if="entry.sfProperties">{{
entry.sfProperties.name }}</span>
<span v-else>There is no corresponding SFLayer entry.</span>
</div>
</div>
<div class="group" v-if="entry.wmProperties">
<span class="group-header">Focus</span>
<div class="full-width">
<span class="key">Focused App:</span>
<span class="value">{{ entry.wmProperties.focusedApp }}</span>
<div />
<span class="key">Focused Activity:</span>
<span class="value">{{ entry.wmProperties.focusedActivity }}</span>
<div />
<span class="key">Focused Window:</span>
<span class="value">{{ entry.wmProperties.focusedWindow }}</span>
<div />
<span class="key" v-if="entry.sfProperties">Focused Window Color:</span>
<span class="value" v-if="entry.sfProperties">{{
entry.sfProperties.focusedWindowRgba
}}</span>
<div />
<span class="key">Input Control Target Frame:</span>
<CoordinatesTable :coordinates="wmControlTargetFrameOrNull" />
<div />
</div>
</div>
<div class="group">
<span class="group-header">Visibility</span>
<div class="full-width">
<span class="key" v-if="entry.wmProperties">InputMethod Window:</span>
<span class="value" v-if="entry.wmProperties">{{
entry.wmProperties.isInputMethodWindowVisible
}}</span>
<div />
<span class="key" v-if="entry.sfProperties">InputMethod Surface:</span>
<span class="value" v-if="entry.sfProperties">{{
entry.sfProperties.isInputMethodSurfaceVisible }}</span>
<div />
</div>
</div>
<div class="group" v-if="entry.sfProperties">
<button
class="text-button group-header"
:class="{ 'selected': isSelected(entry.sfProperties.imeContainer) }"
@click="onClickShowInPropertiesPanel(entry.sfProperties.imeContainer)">
Ime Container
</button>
<div class="full-width">
<span class="key">ZOrderRelativeOfId:</span>
<span class="value">{{
entry.sfProperties.zOrderRelativeOfId
}}</span>
<div />
<span class="key">Z:</span>
<span class="value">{{ entry.sfProperties.z }}</span>
<div />
</div>
</div>
<div class="group" v-if="entry.sfProperties">
<button
class="text-button group-header"
:class="{
'selected': isSelected(entry.sfProperties.inputMethodSurface)
}"
@click="onClickShowInPropertiesPanel(
entry.sfProperties.inputMethodSurface)">
Input Method Surface
</button>
<div class="full-width">
<span class="key">ScreenBounds:</span>
<CoordinatesTable
:coordinates="sfImeContainerScreenBoundsOrNull" />
<div />
<span class="key">Rect:</span>
<CoordinatesTable
:coordinates="sfImeContainerRectOrNull" />
<div />
</div>
</div>
</div>
</template>
<script>
import CoordinatesTable from '@/CoordinatesTable';
import ObjectFormatter from './flickerlib/ObjectFormatter';
import {ObjectTransformer} from '@/transform';
import {getPropertiesForDisplay} from '@/flickerlib/mixin';
import {stableIdCompatibilityFixup} from '@/utils/utils';
function formatProto(obj) {
if (obj?.prettyPrint) {
return obj.prettyPrint();
}
}
export default {
name: 'ImeAdditionalProperties',
components: {CoordinatesTable},
props: ['entry', 'isImeManagerService', 'onSelectItem'],
data() {
return {
selected: null,
};
},
methods: {
getTransformedProperties(item) {
// this function is similar to the one in TraceView.vue,
// but without 'diff visualisation'
ObjectFormatter.displayDefaults = this.displayDefaults;
// TODO(209452852) Refactor both flicker and winscope-native objects to
// implement a common display interface that can be better handled
const target = item.obj ?? item;
const transformer = new ObjectTransformer(
getPropertiesForDisplay(target),
item.name,
stableIdCompatibilityFixup(item),
).setOptions({
skip: item.skip,
formatter: formatProto,
});
return transformer.transform();
},
onClickShowInPropertiesPanel(item) {
this.selected = item;
this.onSelectItem(item);
},
isSelected(item) {
return this.selected === item;
},
},
computed: {
wmProtoOrNull() {
return this.entry.wmProperties?.proto;
},
wmInsetsSourceProviderOrNull() {
return this.entry.wmProperties?.imeInsetsSourceProvider ?
Object.assign({'name': 'Ime Insets Source Provider'},
this.entry.wmProperties.imeInsetsSourceProvider) :
null;
},
wmControlTargetFrameOrNull() {
return this.entry.wmProperties?.imeInsetsSourceProvider
?.insetsSourceProvider?.controlTarget?.windowFrames?.frame || 'null';
},
wmInsetsSourceProviderPositionOrNull() {
return this.entry.wmProperties?.imeInsetsSourceProvider
?.insetsSourceProvider?.control?.position || 'null';
},
wmInsetsSourceProviderIsLeashReadyOrNull() {
return this.entry.wmProperties?.imeInsetsSourceProvider
?.insetsSourceProvider?.isLeashReadyForDispatching || 'null';
},
wmInsetsSourceProviderControllableOrNull() {
return this.entry.wmProperties?.imeInsetsSourceProvider
?.insetsSourceProvider?.controllable || 'null';
},
wmInsetsSourceProviderSourceFrameOrNull() {
return this.entry.wmProperties?.imeInsetsSourceProvider
?.insetsSourceProvider?.source?.frame || 'null';
},
wmInsetsSourceProviderSourceVisibleOrNull() {
return this.entry.wmProperties?.imeInsetsSourceProvider
?.insetsSourceProvider?.source?.visible || 'null';
},
wmInsetsSourceProviderSourceVisibleFrameOrNull() {
return this.entry.wmProperties?.imeInsetsSourceProvider
?.insetsSourceProvider?.source?.visibleFrame || 'null';
},
wmImeControlTargetOrNull() {
return this.entry?.wmProperties?.imeControlTarget ?
Object.assign({'name': 'IME Control Target'},
this.entry.wmProperties.imeControlTarget) :
null;
},
wmImeControlTargetTitleOrNull() {
return this.entry?.wmProperties?.imeControlTarget?.windowContainer
?.identifier?.title || 'null';
},
wmImeInputTargetOrNull() {
return this.entry?.wmProperties?.imeInputTarget ?
Object.assign({'name': 'IME Input Target'},
this.entry.wmProperties.imeInputTarget) :
null;
},
wmImeInputTargetTitleOrNull() {
return this.entry?.wmProperties?.imeInputTarget?.windowContainer
?.identifier?.title || 'null';
},
wmImeLayeringTargetOrNull() {
return this.entry?.wmProperties?.imeLayeringTarget ?
Object.assign({'name': 'IME Layering Target'},
this.entry.wmProperties.imeLayeringTarget) :
null;
},
wmImeLayeringTargetTitleOrNull() {
return this.entry?.wmProperties?.imeLayeringTarget?.windowContainer
?.identifier?.title || 'null';
},
sfImeContainerScreenBoundsOrNull() {
return this.entry.sfProperties?.screenBounds || 'null';
},
sfImeContainerRectOrNull() {
return this.entry.sfProperties?.rect || 'null';
},
isAllPropertiesNull() {
if (this.isImeManagerService) {
return !this.entry.wmProperties;
} else {
return !(this.entry.wmProperties ||
this.entry.sfProperties);
}
},
},
watch: {
entry() {
console.log('Updated IME entry:', this.entry);
},
},
};
</script>
<style scoped>
.container {
overflow: auto;
}
.group {
padding: 0.5rem;
border-bottom: thin solid rgba(0, 0, 0, 0.12);
flex-direction: row;
display: flex;
}
.group .key {
font-weight: 500;
}
.group .value {
color: rgba(0, 0, 0, 0.75);
word-break: break-all !important;
}
.group-header {
justify-content: center;
text-align: left;
padding: 0px 5px;
width: 95px;
display: inline-block;
font-size: bigger;
color: grey;
word-break: break-word;
}
.left-column {
width: 320px;
max-width: 100%;
display: inline-block;
vertical-align: top;
overflow: auto;
padding-right: 20px;
}
.right-column {
width: 320px;
max-width: 100%;
display: inline-block;
vertical-align: top;
overflow: auto;
}
.full-width {
width: 100%;
display: inline-block;
vertical-align: top;
overflow: auto;
}
.column-header {
font-weight: lighter;
font-size: smaller;
}
.element-summary {
padding-top: 1rem;
}
.element-summary .key {
font-weight: 500;
}
.element-summary .value {
color: rgba(0, 0, 0, 0.75);
}
.tree-view {
overflow: auto;
}
.text-button {
border: none;
cursor: pointer;
font-size: 14px;
font-family: roboto;
color: blue;
text-decoration: underline;
text-decoration-color: blue;
background-color: inherit;
}
.text-button:focus {
color: purple;
}
.text-button.selected {
color: purple;
}
</style>

View File

@@ -1,52 +0,0 @@
<!-- 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.
-->
<template>
<TraceView
:store="store"
:file="file"
:summarizer="summarizer"
:presentTags="presentTags"
:presentErrors="presentErrors"
:propertyGroups="false"
:imeAdditionalProperties="this.hasWmSfProperties"
/>
</template>
<script>
import TraceView from "@/TraceView.vue"
export default {
name: "ImeTraceView",
props: ["store", "file", "presentTags", "presentErrors"],
data() {
return {
hasWmSfProperties:
this.file.imeTraceFileClients?.data[0]?.hasWmSfProperties ||
this.file.imeTraceFileService?.data[0]?.hasWmSfProperties ||
this.file.imeTraceFileManagerService?.data[0]?.hasWmSfProperties ||
false,
};
},
components: {
TraceView,
},
methods: {
summarizer(item) {
return null; // TODO: Not sure what to do with this
},
}
}
</script>

View File

@@ -1,188 +0,0 @@
<template>
<div
class="entry"
:class="[
{
'inactive': !source.occured,
'just-inactivated': source.justInactivated,
},
source.level.toLowerCase()
]"
>
<div class="level-column">
<div>
<div class="icon" v-if="source.level.toLowerCase() === 'verbose'">
v
</div>
<i class="material-icons icon" v-else>
{{ levelIcons[source.level.toLowerCase()] }}
</i>
<md-tooltip md-direction="right" style="margin-left: -15px">
{{ source.level.toLowerCase() }}
</md-tooltip>
</div>
</div>
<div class="time-column">
<a @click="setTimelineTime(source.timestamp)" class="time-link">
{{source.time}}
</a>
<div
class="new-badge"
:style="{visibility: source.new ? 'visible' : 'hidden'} "
>
New
</div>
</div>
<div class="tag-column">{{source.tag}}</div>
<div class="at-column">{{source.at}}</div>
<div class="message-column">{{source.text}}</div>
</div>
</template>
<script>
import {logLevel} from './utils/consts';
export default {
name: 'logentry',
props: {
index: {
type: Number,
},
source: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
levelIcons: {
[logLevel.INFO]: 'info_outline',
[logLevel.DEBUG]: 'help_outline',
[logLevel.VERBOSE]: 'assignment',
[logLevel.WARN]: 'warning',
[logLevel.ERROR]: 'error',
[logLevel.WTF]: 'bolt',
},
};
},
methods: {
setTimelineTime(timestamp) {
this.$store.dispatch('updateTimelineTime', timestamp);
},
},
};
</script>
<style scoped>
.level-column {
width: 2em;
display: inline-flex;
}
.level-column > div {
align-self: start;
}
.time-column {
display: inline-flex;
width: 13em;
}
.time-column .time-link {
width: 9em;
}
.tag-column {
width: 11em;
min-width: 11em;
}
.at-column {
width: 30em;
min-width: 30em;
}
.message-column {
min-width: 50em;
flex-grow: 1;
word-wrap: break-word;
}
.entry {
width: 100%;
display: inline-flex;
}
.entry:hover {
background: #f1f1f1;
}
.entry > div {
padding: 6px 10px;
border-bottom: 1px solid #f1f1f1;
}
a {
cursor: pointer;
}
.inactive {
color: gray;
}
.inactive a {
color: gray;
}
.just-inactivated {
background: #dee2e3;
}
.new-badge {
display: inline-block;
background: rgb(84, 139, 247);
border-radius: 3px;
color: white;
padding: 0 5px;
margin-left: 5px;
font-size: 10px;
align-self: flex-start;
}
.entry.warn, .entry.warn > div {
background: #FFE0B2;
}
.entry.warn.inactive, .entry.warn.inactive > div {
background: #FFF3E0;
}
.entry.error, .entry.error > div,
.entry.wtf, .entry.wtf > div {
background: #FFCCBC;
}
.entry.error.inactive, .entry.error.inactive > div,
.entry.wtf.inactive, .entry.wtf.inactive > div {
background: #FBE9E7;
}
.level-column .icon {
font-size: 15px;
color: gray;
width: 15px;
height: 15px;
text-align: center;
}
.entry.warn .level-column .icon {
color: #FBC02D;
font-size: 20px;
}
.entry.error .level-column .icon, .entry.wtf .level-column .icon {
color: #FF6E40;
font-size: 20px;
}
</style>

View File

@@ -1,300 +0,0 @@
<!-- Copyright (C) 2019 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.
-->
<template>
<md-card-content class="container">
<div class="navigation">
<md-content
md-tag="md-toolbar"
md-elevation="0"
class="card-toolbar md-transparent md-dense"
>
<h2 class="md-title" style="flex: 1">Log View</h2>
<md-button
class="md-dense md-primary"
@click.native="scrollToRow(lastOccuredVisibleIndex)"
>
Jump to latest entry
</md-button>
<md-button
class="md-icon-button" :class="{'md-primary': pinnedToLatest}"
@click.native="togglePin"
>
<md-icon>push_pin</md-icon>
<md-tooltip md-direction="top" v-if="pinnedToLatest">
Unpin to latest message
</md-tooltip>
<md-tooltip md-direction="top" v-else>
Pin to latest message
</md-tooltip>
</md-button>
</md-content>
</div>
<div class="filters">
<md-field>
<label>Log Levels</label>
<md-select v-model="selectedLogLevels" multiple>
<md-option v-for="level in logLevels" :value="level" v-bind:key="level">{{ level }}</md-option>
</md-select>
</md-field>
<md-field>
<label>Tags</label>
<md-select v-model="selectedTags" multiple>
<md-option v-for="tag in tags" :value="tag" v-bind:key="tag">{{ tag }}</md-option>
</md-select>
</md-field>
<md-autocomplete v-model="selectedSourceFile" :md-options="sourceFiles">
<label>Source file</label>
<template slot="md-autocomplete-item" slot-scope="{ item, term }">
<md-highlight-text :md-term="term">{{ item }}</md-highlight-text>
</template>
<template slot="md-autocomplete-empty" slot-scope="{ term }">
No source file matching "{{ term }}" was found.
</template>
</md-autocomplete>
<md-field class="search-message-field" md-clearable>
<md-input placeholder="Search messages..." v-model="searchInput"></md-input>
</md-field>
</div>
<div v-if="processedData.length > 0" style="overflow-y: auto;">
<virtual-list style="height: 600px; overflow-y: auto;"
:data-key="'uid'"
:data-sources="processedData"
:data-component="logEntryComponent"
ref="loglist"
/>
</div>
<div class="no-logs-message" v-else>
<md-icon>error_outline</md-icon>
<span class="message">No logs founds...</span>
</div>
</md-card-content>
</template>
<script>
import { findLastMatchingSorted } from './utils/utils.js';
import { logLevel } from './utils/consts';
import LogEntryComponent from './LogEntry.vue';
import VirtualList from '../libs/virtualList/VirtualList';
export default {
name: 'logview',
data() {
const data = this.file.data;
// Record analytics event
this.recordOpenTraceEvent("ProtoLog");
const tags = new Set();
const sourceFiles = new Set();
for (const line of data) {
tags.add(line.tag);
sourceFiles.add(line.at);
}
data.forEach((entry, index) => entry.index = index);
const logLevels = Object.values(logLevel);
return {
data,
isSelected: false,
prevLastOccuredIndex: -1,
lastOccuredIndex: 0,
selectedTags: [],
selectedSourceFile: null,
searchInput: null,
sourceFiles: Object.freeze(Array.from(sourceFiles)),
tags: Object.freeze(Array.from(tags)),
pinnedToLatest: true,
logEntryComponent: LogEntryComponent,
logLevels,
selectedLogLevels: [],
}
},
methods: {
arrowUp() {
this.isSelected = !this.isSelected;
return !this.isSelected;
},
arrowDown() {
this.isSelected = !this.isSelected;
return !this.isSelected;
},
getRowEl(idx) {
return this.$refs.tableBody.querySelectorAll('tr')[idx];
},
togglePin() {
this.pinnedToLatest = !this.pinnedToLatest;
},
scrollToRow(index) {
if (!this.$refs.loglist) {
return;
}
const itemOffset = this.$refs.loglist.virtual.getOffset(index);
const itemSize = 35;
const loglistSize = this.$refs.loglist.getClientSize();
this.$refs.loglist.scrollToOffset(itemOffset - loglistSize + itemSize);
},
getLastOccuredIndex(data, timestamp) {
if (this.data.length === 0) {
return 0;
}
return findLastMatchingSorted(data,
(array, idx) => array[idx].timestamp <= timestamp);
},
},
watch: {
pinnedToLatest(isPinned) {
if (isPinned) {
this.scrollToRow(this.lastOccuredVisibleIndex);
}
},
currentTimestamp: {
immediate: true,
handler(newTimestamp) {
this.prevLastOccuredIndex = this.lastOccuredIndex;
this.lastOccuredIndex = this.getLastOccuredIndex(this.data, newTimestamp);
if (this.pinnedToLatest) {
this.scrollToRow(this.lastOccuredVisibleIndex);
}
},
}
},
props: ['file'],
computed: {
lastOccuredVisibleIndex() {
return this.getLastOccuredIndex(this.processedData, this.currentTimestamp);
},
currentTimestamp() {
return this.$store.state.currentTimestamp;
},
processedData() {
const filteredData = this.data.filter(line => {
if (this.selectedLogLevels.length > 0 &&
!this.selectedLogLevels.includes(line.level.toLowerCase())) {
return false;
}
if (this.sourceFiles.includes(this.selectedSourceFile)) {
// Only filter once source file is fully inputed
if (line.at != this.selectedSourceFile) {
return false;
}
}
if (this.selectedTags.length > 0 && !this.selectedTags.includes(line.tag)) {
return false;
}
if (this.searchInput && !line.text.includes(this.searchInput)) {
return false;
}
return true;
});
for (const entry of filteredData) {
entry.new = this.prevLastOccuredIndex < entry.index &&
entry.index <= this.lastOccuredIndex;
entry.occured = entry.index <= this.lastOccuredIndex;
entry.justInactivated = this.lastOccuredIndex < entry.index &&
entry.index <= this.prevLastOccuredIndex;
// Force refresh if any of these changes
entry.uid = `${entry.index}${entry.new ? '-new' : ''}${entry.index}${entry.justInactivated ? '-just-inactivated' : ''}${entry.occured ? '-occured' : ''}`
}
return filteredData;
}
},
components: {
'virtual-list': VirtualList,
'logentry': LogEntryComponent,
}
}
</script>
<style>
.container {
display: flex;
flex-wrap: wrap;
}
.filters, .navigation {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
.navigation {
justify-content: flex-end;
}
.navigation > button {
margin: 0;
}
.filters > div {
margin: 10px;
}
.log-header {
display: inline-flex;
color: var(--md-theme-default-text-accent-on-background, rgba(0,0,0,0.54));
font-weight: bold;
}
.log-header > div {
padding: 6px 10px;
border-bottom: 1px solid #f1f1f1;
}
.log-header .time-column {
width: 13em;
}
.log-header .tag-column {
width: 10em;
}
.log-header .at-column {
width: 30em;
}
.column-title {
font-size: 12px;
}
.no-logs-message {
margin: 15px;
display: flex;
align-content: center;
align-items: center;
}
.no-logs-message .message {
margin-left: 10px;
font-size: 15px;
}
</style>

View File

@@ -1,91 +0,0 @@
<template>
<vue-context ref="menu">
</vue-context>
</template>
<script>
import VueContext from 'vue-context';
export default {
name: 'NodeContextMenu',
components: {
VueContext,
},
methods: {
open(e) {
this.$refs.menu.open(e);
},
close() {
this.$refs.menu.close();
},
},
};
</script>
<style scoped>
.v-context,
.v-context ul {
background-color: #fff;
background-clip: padding-box;
border-radius: .25rem;
border: 1px solid rgba(0, 0, 0, .15);
box-shadow:
0 2px 2px 0 rgba(0, 0, 0, .14),
0 3px 1px -2px rgba(0, 0, 0, .2),
0 1px 5px 0 rgba(0, 0, 0, .12);
display: block;
margin: 0;
padding: 10px 0;
min-width: 10rem;
z-index: 10;
position: fixed;
list-style: none;
box-sizing: border-box;
max-height: calc(100% - 50px);
overflow-y: auto
}
.v-context>li,
.v-context ul>li {
margin: 0;
position: relative
}
.v-context>li>a,
.v-context ul>li>a {
display: block;
padding: .5rem 1.5rem;
font-weight: 400;
color: #212529;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border: 0
}
.v-context>li>a:focus,
.v-context>li>a:hover,
.v-context ul>li>a:focus,
.v-context ul>li>a:hover {
text-decoration: none;
color: #212529;
background-color: #f8f9fa
}
.v-context:focus,
.v-context>li>a:focus,
.v-context ul:focus,
.v-context ul>li>a:focus {
outline: 0
}
.v-context__sub>a:after {
content: "\2BC8";
float: right;
padding-left: 1rem
}
.v-context__sub>ul {
display: none
}
</style>

View File

@@ -1,960 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<div class="overlay" v-if="hasTimeline || video">
<div class="overlay-content" ref="overlayContent">
<draggable-div
ref="videoOverlay"
class="video-overlay"
v-show="minimized && showVideoOverlay"
position="bottomLeft"
:asyncLoad="true"
:resizeable="true"
v-on:requestExtraWidth="updateVideoOverlayWidth"
:style="videoOverlayStyle"
v-if="video"
>
<template slot="header">
<div class="close-video-overlay" @click="closeVideoOverlay">
<md-icon>
close
<md-tooltip md-direction="right">Close video overlay</md-tooltip>
</md-icon>
</div>
</template>
<template slot="main">
<div ref="overlayVideoContainer">
<videoview
ref="video"
:file="video"
:height="videoHeight"
@loaded="videoLoaded" />
</div>
</template>
</draggable-div>
</div>
<md-bottom-bar
class="bottom-nav"
v-if="hasTimeline || (video && !showVideoOverlay)"
ref="bottomNav"
>
<div class="nav-content">
<div class="">
<searchbar
class="search-bar"
v-if="search"
:searchTypes="searchTypes"
:store="store"
:presentTags="Object.freeze(presentTags)"
:presentErrors="Object.freeze(presentErrors)"
:timeline="mergedTimeline.timeline"
/>
<md-toolbar
md-elevation="0"
class="md-transparent">
<md-button
@click="toggleSearch()"
class="drop-search"
>
Toggle search bar
</md-button>
<div class="toolbar" :class="{ expanded: expanded }">
<div class="resize-bar" v-show="expanded">
<div v-if="video" @mousedown="resizeBottomNav">
<md-icon class="drag-handle">
drag_handle
<md-tooltip md-direction="top">resize</md-tooltip>
</md-icon>
</div>
</div>
<div class="active-timeline" v-show="minimized">
<div
class="active-timeline-icon"
@click="$refs.navigationTypeSelection.$el
.querySelector('input').click()"
>
<md-icon class="collapsed-timeline-icon">
{{ collapsedTimelineIcon }}
<md-tooltip>
{{ collapsedTimelineIconTooltip }}
</md-tooltip>
</md-icon>
</div>
<md-field
v-if="multipleTraces"
ref="navigationTypeSelection"
class="navigation-style-selection-field"
>
<label>Navigation</label>
<md-select
v-model="navigationStyle"
name="navigationStyle"
md-dense
>
<md-icon-option
:value="NAVIGATION_STYLE.GLOBAL"
icon="public"
desc="Consider all timelines for navigation"
/>
<md-icon-option
:value="NAVIGATION_STYLE.FOCUSED"
:icon="TRACE_ICONS[focusedFile.type]"
:desc="`Automatically switch what timeline is considered
for navigation based on what is visible on screen.
Currently ${focusedFile.type}.`"
/>
<!-- TODO: Add edit button for custom settings that opens
popup dialog menu -->
<md-icon-option
:value="NAVIGATION_STYLE.CUSTOM"
icon="dashboard_customize"
desc="Considers only the enabled timelines for
navigation. Expand the bottom bar to toggle
timelines."
/>
<md-optgroup label="Targeted">
<md-icon-option
v-for="file in timelineFiles"
v-bind:key="file.type"
:value="`${NAVIGATION_STYLE.TARGETED}-` +
`${file.type}`"
:displayValue="file.type"
:shortValue="NAVIGATION_STYLE.TARGETED"
:icon="TRACE_ICONS[file.type]"
:desc="`Only consider ${file.type} ` +
'for timeline navigation.'"
/>
</md-optgroup>
</md-select>
</md-field>
</div>
<div
class="minimized-timeline-content"
v-show="minimized"
v-if="hasTimeline"
>
<input
class="timestamp-search-input"
v-model="searchInput"
spellcheck="false"
:placeholder="seekTime"
@focus="updateInputMode(true)"
@blur="updateInputMode(false)"
@keyup.enter="updateSearchForTimestamp"
/>
<timeline
:store="store"
:flickerMode="flickerMode"
:tags="Object.freeze(presentTags)"
:errors="Object.freeze(presentErrors)"
:timeline="Object.freeze(minimizedTimeline.timeline)"
:selected-index="minimizedTimeline.selectedIndex"
:scale="scale"
:crop="crop"
class="minimized-timeline"
/>
</div>
<md-button
class="md-icon-button show-video-overlay-btn"
:class="{active: minimized && showVideoOverlay}"
@click="toggleVideoOverlay"
v-show="minimized"
style="margin-bottom: 10px;"
>
<i class="md-icon md-icon-font">
featured_video
</i>
<md-tooltip md-direction="top">
<span v-if="showVideoOverlay">Hide video overlay</span>
<span v-else>Show video overlay</span>
</md-tooltip>
</md-button>
<md-button
class="md-icon-button toggle-btn"
@click="toggle"
style="margin-bottom: 10px;"
>
<md-icon v-if="minimized">
expand_less
<md-tooltip md-direction="top" @click="recordButtonClickedEvent(`Expand Timeline`)">Expand timeline</md-tooltip>
</md-icon>
<md-icon v-else>
expand_more
<md-tooltip md-direction="top" @click="recordButtonClickedEvent(`Collapse Timeline`)">Collapse timeline</md-tooltip>
</md-icon>
</md-button>
</div>
</md-toolbar>
<div class="expanded-content" v-show="expanded">
<div :v-if="video">
<div
class="expanded-content-video"
ref="expandedContentVideoContainer"
>
<!-- Video moved here on expansion -->
</div>
</div>
<div class="flex-fill">
<div
ref="expandedTimeline"
:style="`padding-top: ${resizeOffset}px;`"
>
<div class="seek-time" v-if="seekTime">
<b>Seek time: </b>
<input
class="timestamp-search-input"
:class="{ expanded: expanded }"
v-model="searchInput"
spellcheck="false"
:placeholder="seekTime"
@focus="updateInputMode(true)"
@blur="updateInputMode(false)"
@keyup.enter="updateSearchForTimestamp"
/>
</div>
<timelines
:timelineFiles="timelineFiles"
:scale="scale"
:crop="crop"
:cropIntent="cropIntent"
v-on:crop="onTimelineCrop"
/>
<div class="timeline-selection">
<div class="timeline-selection-header">
<label style="user-select: none;">
Timeline Area Selection
</label>
<span class="material-icons help-icon">
help_outline
<md-tooltip md-direction="right">
Select the area of the timeline to focus on.
Click and drag to select.
</md-tooltip>
</span>
<md-button
class="md-primary"
v-if="isCropped"
@click.native="clearSelection"
>
Clear selection
</md-button>
</div>
<timeline-selection
:timeline="mergedTimeline.timeline"
:start-timestamp="0"
:end-timestamp="0"
:scale="scale"
:cropArea="crop"
v-on:crop="onTimelineCrop"
v-on:cropIntent="onTimelineCropIntent"
v-on:showVideoAt="changeVideoTimestamp"
v-on:resetVideoTimestamp="resetVideoTimestamp"
/>
</div>
<div class="help" v-if="!minimized">
<div class="help-icon-wrapper">
<span class="material-icons help-icon">
help_outline
<md-tooltip md-direction="left">
Click on icons to disable timelines
</md-tooltip>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</md-bottom-bar>
</div>
</template>
<script>
import Timeline from './Timeline.vue';
import Timelines from './Timelines.vue';
import TimelineSelection from './TimelineSelection.vue';
import DraggableDiv from './DraggableDiv.vue';
import VideoView from './VideoView.vue';
import MdIconOption from './components/IconSelection/IconSelectOption.vue';
import Searchbar from './Searchbar.vue';
import FileType from './mixins/FileType.js';
import {NAVIGATION_STYLE} from './utils/consts';
import {TRACE_ICONS} from '@/decode.js';
// eslint-disable-next-line camelcase
import {nanos_to_string, getClosestTimestamp} from './transform.js';
export default {
name: 'overlay',
props: ['store', 'presentTags', 'presentErrors', 'searchTypes'],
mixins: [FileType],
data() {
return {
minimized: true,
// height of video in expanded timeline,
// made to match expandedTimeline dynamically
videoHeight: 'auto',
dragState: {
clientY: null,
lastDragEndPosition: null,
},
resizeOffset: 0,
showVideoOverlay: true,
mergedTimeline: null,
NAVIGATION_STYLE,
navigationStyle: this.store.navigationStyle,
videoOverlayExtraWidth: 0,
crop: null,
cropIntent: null,
TRACE_ICONS,
search: false,
searchInput: "",
isSeekTimeInputMode: false,
};
},
created() {
this.mergedTimeline = this.computeMergedTimeline();
this.$store.commit('setMergedTimeline', this.mergedTimeline);
this.updateNavigationFileFilter();
},
mounted() {
this.emitBottomHeightUpdate();
},
destroyed() {
this.$store.commit('removeMergedTimeline', this.mergedTimeline);
this.updateInputMode(false);
},
watch: {
navigationStyle(style) {
// Only store navigation type in local store if it's a type that will
// work regardless of what data is loaded.
if (style === NAVIGATION_STYLE.GLOBAL ||
style === NAVIGATION_STYLE.FOCUSED) {
this.store.navigationStyle = style;
}
this.updateNavigationFileFilter();
},
minimized() {
// Minimized toggled
this.updateNavigationFileFilter();
this.$nextTick(this.emitBottomHeightUpdate);
},
},
computed: {
video() {
return this.$store.getters.video;
},
videoOverlayStyle() {
return {
width: 150 + this.videoOverlayExtraWidth + 'px',
};
},
timelineFiles() {
return this.$store.getters.timelineFiles;
},
focusedFile() {
return this.$store.state.focusedFile;
},
expanded() {
return !this.minimized;
},
seekTime() {
return nanos_to_string(this.currentTimestamp);
},
scale() {
const mx = Math.max(...(this.timelineFiles.map((f) =>
Math.max(...f.timeline))));
const mi = Math.min(...(this.timelineFiles.map((f) =>
Math.min(...f.timeline))));
return [mi, mx];
},
currentTimestamp() {
return this.$store.state.currentTimestamp;
},
hasTimeline() {
// Returns true if a meaningful timeline exists (i.e. not only dumps)
for (const file of this.timelineFiles) {
if (file.timeline.length > 0 &&
(file.timeline[0] !== undefined || file.timeline.length > 1)) {
return true;
}
}
return false;
},
collapsedTimelineIconTooltip() {
switch (this.navigationStyle) {
case NAVIGATION_STYLE.GLOBAL:
return 'All timelines';
case NAVIGATION_STYLE.FOCUSED:
return `Focused: ${this.focusedFile.type}`;
case NAVIGATION_STYLE.CUSTOM:
return 'Enabled timelines';
default:
const split = this.navigationStyle.split('-');
if (split[0] !== NAVIGATION_STYLE.TARGETED) {
console.warn('Unexpected navigation type; fallback to global');
return 'All timelines';
}
const fileType = split[1];
return fileType;
}
},
collapsedTimelineIcon() {
switch (this.navigationStyle) {
case NAVIGATION_STYLE.GLOBAL:
return 'public';
case NAVIGATION_STYLE.FOCUSED:
return TRACE_ICONS[this.focusedFile.type];
case NAVIGATION_STYLE.CUSTOM:
return 'dashboard_customize';
default:
const split = this.navigationStyle.split('-');
if (split[0] !== NAVIGATION_STYLE.TARGETED) {
console.warn('Unexpected navigation type; fallback to global');
return 'public';
}
const fileType = split[1];
return TRACE_ICONS[fileType];
}
},
minimizedTimeline() {
if (this.navigationStyle === NAVIGATION_STYLE.GLOBAL) {
return this.mergedTimeline;
}
if (this.navigationStyle === NAVIGATION_STYLE.FOCUSED) {
//dumps do not have a timeline, so if scrolling over a dump, show merged timeline
if (this.focusedFile.timeline) {
return this.focusedFile;
}
return this.mergedTimeline;
}
if (this.navigationStyle === NAVIGATION_STYLE.CUSTOM) {
// TODO: Return custom timeline
return this.mergedTimeline;
}
if (
this.navigationStyle.split('-').length >= 2
&& this.navigationStyle.split('-')[0] === NAVIGATION_STYLE.TARGETED
) {
return this.$store.state
.traces[this.navigationStyle.split('-')[1]];
}
console.warn('Unexpected navigation type; fallback to global');
return this.mergedTimeline;
},
isCropped() {
return this.crop != null &&
(this.crop.left !== 0 || this.crop.right !== 1);
},
multipleTraces() {
return this.timelineFiles.length > 1;
},
flickerMode() {
return this.presentTags.length>0 || this.presentErrors.length>0;
},
},
updated() {
this.$nextTick(() => {
if (this.$refs.expandedTimeline && this.expanded) {
this.videoHeight = this.$refs.expandedTimeline.clientHeight;
} else {
this.videoHeight = 'auto';
}
});
},
methods: {
toggleSearch() {
this.search = !(this.search);
this.recordButtonClickedEvent("Toggle Search Bar");
},
/**
* determines whether left/right arrow keys should move cursor in input field
* and upon click of input field, fills with current timestamp
*/
updateInputMode(isInputMode) {
this.isSeekTimeInputMode = isInputMode;
this.store.isInputMode = isInputMode;
if (!isInputMode) {
this.searchInput = "";
} else {
this.searchInput = this.seekTime;
}
},
/** Navigates to closest timestamp in timeline to search input*/
updateSearchForTimestamp() {
const closestTimestamp = getClosestTimestamp(this.searchInput, this.mergedTimeline.timeline);
this.$store.dispatch("updateTimelineTime", closestTimestamp);
this.updateInputMode(false);
this.recordNewEvent("Searching for timestamp")
},
emitBottomHeightUpdate() {
if (this.$refs.bottomNav) {
const newHeight = this.$refs.bottomNav.$el.clientHeight;
this.$emit('bottom-nav-height-change', newHeight);
}
},
computeMergedTimeline() {
const mergedTimeline = {
timeline: [], // Array of integers timestamps
selectedIndex: 0,
};
const timelineIndexes = [];
const timelines = [];
for (const file of this.timelineFiles) {
timelineIndexes.push(0);
timelines.push(file.timeline);
}
var timelineToAdvance = 0;
while (timelineToAdvance !== undefined) {
timelineToAdvance = undefined;
let minTime = Infinity;
for (let i = 0; i < timelines.length; i++) {
const timeline = timelines[i];
const index = timelineIndexes[i];
if (index >= timeline.length) {
continue;
}
const time = timeline[index];
if (time < minTime) {
minTime = time;
timelineToAdvance = i;
}
}
if (timelineToAdvance === undefined) {
// No more elements left
break;
}
timelineIndexes[timelineToAdvance]++;
mergedTimeline.timeline.push(minTime);
}
// Object is frozen for performance reasons
// It will prevent Vue from making it a reactive object which will be very
// slow as the timeline gets larger.
Object.freeze(mergedTimeline.timeline);
return mergedTimeline;
},
toggle() {
this.minimized ? this.expand() : this.minimize();
this.minimized = !this.minimized;
},
expand() {
if (this.video) {
this.$refs.expandedContentVideoContainer
.appendChild(this.$refs.video.$el);
}
},
minimize() {
if (this.video) {
this.$refs.overlayVideoContainer.appendChild(this.$refs.video.$el);
}
},
fileIsVisible(f) {
return this.visibleDataViews.includes(f.filename);
},
resizeBottomNav(e) {
this.initResizeAction(e);
},
initResizeAction(e) {
document.onmousemove = this.startResize;
document.onmouseup = this.endResize;
},
startResize(e) {
if (this.dragState.clientY === null) {
this.dragState.clientY = e.clientY;
}
const movement = this.dragState.clientY - e.clientY;
const resizeOffset = this.resizeOffset + movement;
if (resizeOffset < 0) {
this.resizeOffset = 0;
this.dragState.clientY = null;
} else if (movement > this.getBottomNavDistanceToTop()) {
this.dragState.clientY += this.getBottomNavDistanceToTop();
this.resizeOffset += this.getBottomNavDistanceToTop();
} else {
this.resizeOffset = resizeOffset;
this.dragState.clientY = e.clientY;
}
},
endResize() {
this.dragState.lastDragEndPosition = this.dragState.clientY;
this.dragState.clientY = null;
document.onmouseup = null;
document.onmousemove = null;
},
getBottomNavDistanceToTop() {
return this.$refs.bottomNav.$el.getBoundingClientRect().top;
},
closeVideoOverlay() {
this.showVideoOverlay = false;
this.recordButtonClickedEvent("Close Video Overlay")
},
openVideoOverlay() {
this.showVideoOverlay = true;
this.recordButtonClickedEvent("Open Video Overlay")
},
toggleVideoOverlay() {
this.showVideoOverlay = !this.showVideoOverlay;
this.recordButtonClickedEvent("Toggle Video Overlay")
},
videoLoaded() {
this.$refs.videoOverlay.contentLoaded();
},
updateNavigationFileFilter() {
if (!this.minimized) {
// Always use custom mode navigation when timeline is expanded
this.$store.commit('setNavigationFilesFilter',
(f) => !f.timelineDisabled);
return;
}
let navigationStyleFilter;
switch (this.navigationStyle) {
case NAVIGATION_STYLE.GLOBAL:
navigationStyleFilter = (f) => true;
break;
case NAVIGATION_STYLE.FOCUSED:
navigationStyleFilter =
(f) => f.type === this.focusedFile.type;
break;
case NAVIGATION_STYLE.CUSTOM:
navigationStyleFilter = (f) => !f.timelineDisabled;
break;
default:
const split = this.navigationStyle.split('-');
if (split[0] !== NAVIGATION_STYLE.TARGETED) {
console.warn('Unexpected navigation type; fallback to global');
navigationStyleFilter = (f) => true;
break;
}
const fileType = split[1];
navigationStyleFilter =
(f) => f.type === fileType;
}
this.recordChangedNavigationStyleEvent(this.navigationStyle);
this.$store.commit('setNavigationFilesFilter', navigationStyleFilter);
},
updateVideoOverlayWidth(width) {
this.videoOverlayExtraWidth = width;
},
onTimelineCrop(cropDetails) {
this.crop = cropDetails;
},
onTimelineCropIntent(cropIntent) {
this.cropIntent = cropIntent;
},
changeVideoTimestamp(ts) {
if (!this.$refs.video) {
return;
}
this.$refs.video.selectFrameAtTime(ts);
},
resetVideoTimestamp() {
if (!this.$refs.video) {
return;
}
this.$refs.video.jumpToSelectedIndex();
},
clearSelection() {
this.crop = null;
},
},
components: {
'timeline': Timeline,
'timelines': Timelines,
'timeline-selection': TimelineSelection,
'videoview': VideoView,
'draggable-div': DraggableDiv,
'md-icon-option': MdIconOption,
'searchbar': Searchbar,
},
};
</script>
<style scoped>
.overlay {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100vw;
height: 100vh;
z-index: 10;
margin: 0;
display: flex;
flex-direction: column;
pointer-events: none;
}
.overlay-content {
flex-grow: 1;
z-index: 10;
}
.bottom-nav {
background: white;
margin: 0;
max-height: 100vh;
bottom: 0;
left: 0;
pointer-events: all;
}
.nav-content {
width: 100%;
}
.toolbar, .active-timeline, .options {
display: flex;
flex-direction: row;
flex: 1;
align-items: center;
}
.toolbar.expanded {
align-items: baseline;
}
.minimized-timeline-content {
flex-grow: 1;
}
.minimized-timeline-content .seek-time {
padding: 3px 0;
}
.options, .expanded-content .seek-time {
padding: 0 20px 15px 20px;
}
.options label {
font-weight: 600;
}
.options .datafilter {
height: 50px;
display: flex;
align-items: center;
}
.expanded-content {
display: flex;
}
.flex-fill {
flex-grow: 1;
}
.video {
flex-grow: 0;
}
.resize-bar {
flex-grow: 1;
}
.drag-handle {
cursor: grab;
}
.md-icon-button {
margin: 0;
}
.toggle-btn {
margin-left: 8px;
align-self: flex-end;
}
.video-overlay {
display: inline-block;
margin-bottom: 15px;
min-width: 50px;
max-width: 50vw;
height: auto;
resize: horizontal;
pointer-events: all;
}
.close-video-overlay {
float: right;
cursor: pointer;
}
.show-video-overlay-btn {
margin-left: 12px;
margin-right: -8px;
align-self: flex-end;
}
.show-video-overlay-btn .md-icon {
color: #9E9E9E!important;
}
.collapsed-timeline-icon {
cursor: pointer;
}
.show-video-overlay-btn.active .md-icon {
color: #212121!important;
}
.help {
display: flex;
align-content: flex-end;
align-items: flex-end;
flex-direction: column;
}
.help-icon-wrapper {
margin-right: 20px;
margin-bottom: 10px;
user-select: none;
}
.help-icon-wrapper .help-icon {
cursor: help;
user-select: none;
}
.trace-icon {
cursor: pointer;
user-select: none;
}
.trace-icon.disabled {
color: gray;
}
.active-timeline {
flex: 0 0 auto;
}
.active-timeline .icon {
margin-right: 20px;
}
.active-timeline .active-timeline-icon {
margin-right: 10px;
align-self: flex-end;
margin-bottom: 18px;
}
.minimized-timeline-content {
align-self: flex-start;
padding-top: 1px;
}
.minimized-timeline-content label {
color: rgba(0,0,0,0.54);
font-size: 12px;
font-family: inherit;
cursor: text;
}
.minimized-timeline-content .minimized-timeline {
margin-top: 4px;
}
.navigation-style-selection-field {
width: 90px;
margin-right: 10px;
margin-bottom: 0;
}
.timeline-selection-header {
display: flex;
align-items: center;
padding-left: 15px;
height: 48px;
}
.help-icon {
font-size: 15px;
margin-bottom: 15px;
cursor: help;
user-select: none;
}
.timestamp-search-input {
outline: none;
border-width: 0 0 1px;
border-color: gray;
font-family: inherit;
color: #448aff;
font-size: 12px;
padding: 0;
letter-spacing: inherit;
width: 125px;
}
.timestamp-search-input:focus {
border-color: #448aff;
}
.timestamp-search-input.expanded {
font-size: 14px;
width: 150px;
}
.drop-search:hover {
background-color: #9af39f;
}
</style>

View File

@@ -1,74 +0,0 @@
<!-- 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.
-->
<template>
<div class="properties-table-wrapper">
<md-table class="table">
<md-table-row
v-for="[propertyName, propertyValue] in propertyEntries"
:key="propertyName">
<md-table-cell class="table-key-cell">
{{ propertyName }}
</md-table-cell>
<md-table-cell class="table-key-value">
{{ propertyValue != null ? propertyValue : 'undefined' }}
</md-table-cell>
</md-table-row>
</md-table>
</div>
</template>
<script>
export default {
name: 'PropertiesTableView.vue',
props: ['tableEntries'],
computed: {
propertyEntries() {
return Object.entries(this.tableEntries);
},
},
};
</script>
<style>
.properties-table-wrapper {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
margin-bottom: 10px;
}
.properties-table-wrapper .table-heading {
padding: 10px 16px;
}
.properties-table-wrapper .table-key-cell {
background-color: rgba(158, 192, 200, 0.281);
}
.properties-table-wrapper .table-key-value {
overflow-wrap: anywhere;
border-left: 1px solid rgba(0, 0, 0, 0.12);
}
.properties-table-wrapper .md-table-cell {
height: auto;
}
.properties-table-wrapper .md-table-cell-container {
padding: 8px 16px;
}
.properties-table-wrapper .md-table-cell:last-child .md-table-cell-container {
padding-right: 16px;
}
</style>

View File

@@ -1,84 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<span>
<span class="key">{{ key }} </span>
<span v-if="value">: </span>
<span class="value" v-if="value" :class="[valueClass]">{{ value }}</span>
</span>
</template>
<script>
export default {
name: 'PropertiesTreeElement',
props: ['item', 'simplify-names'],
computed: {
key() {
if (!this.item.children || this.item.children.length === 0) {
return this.item.name.split(': ')[0];
}
return this.item.name;
},
value() {
if (!this.item.children || this.item.children.length === 0) {
return this.item.name.split(': ').slice(1).join(': ');
}
return null;
},
valueClass() {
if (!this.value) {
return null;
}
if (this.value == 'null') {
return 'null';
}
if (this.value == 'true') {
return 'true';
}
if (this.value == 'false') {
return 'false';
}
if (!isNaN(this.value)) {
return 'number';
}
},
},
};
</script>
<style scoped>
.key {
color: #4b4b4b;
}
.value {
color: #8A2BE2;
}
.value.null {
color: #e1e1e1;
}
.value.number {
color: #4c75fd;
}
.value.true {
color: #2ECC40;
}
.value.false {
color: #FF4136;
}
</style>

View File

@@ -1,160 +0,0 @@
<!-- Copyright (C) 2017 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.
-->
<template>
<div class="bounds" :style="boundsStyle">
<div
class="rect" v-for="rect in filteredRects"
:style="rectToStyle(rect)"
@click="onClick(rect)"
v-bind:key="`${rect.left}-${rect.right}-${rect.top}-${rect.bottom}-${rect.ref.name}`"
>
<span class="label">{{rect.label}}</span>
</div>
<div
class="highlight"
v-if="highlight"
:style="rectToStyle(highlight)"
/>
<div
class="displayRect" v-for="rect in displayRects"
:style="rectToStyle(rect)"
v-bind:key="`${rect.left}-${rect.right}-${rect.top}-${rect.bottom}-${rect.id}`"
/>
</div>
</template>
<script>
// eslint-disable-next-line camelcase
import {multiplyRect} from './matrix_utils.js';
export default {
name: 'rects',
props: ['bounds', 'rects', 'highlight','displays'],
data() {
return {
desiredHeight: 800,
desiredWidth: 400,
};
},
computed: {
boundsC() {
if (this.bounds) {
return this.bounds;
}
var width = Math.max(
...this.rects.map((rect) => multiplyRect(rect.transform, rect).right));
var height = Math.max(
...this.rects.map((rect) => multiplyRect(rect.transform, rect).bottom));
// constrain max bounds to prevent boundless layers from shrinking visible displays
if (this.hasDisplays) {
width = Math.min(width, this.maxWidth);
height = Math.min(height, this.maxHeight);
}
return {width, height};
},
maxWidth() {
return Math.max(...this.displayRects.map(rect => rect.width)) * 1.3;
},
maxHeight() {
return Math.max(...this.displayRects.map(rect => rect.height)) * 1.3;
},
hasDisplays() {
return this.displays.length > 0;
},
boundsStyle() {
return this.rectToStyle({top: 0, left: 0, right: this.boundsC.width,
bottom: this.boundsC.height});
},
filteredRects() {
return this.rects.filter((rect) => {
const isVisible = rect.ref.isVisible;
return isVisible;
});
},
displayRects() {
return this.displays.map(display => {
var rect = display.layerStackSpace;
rect.id = display.id;
return rect;
});
},
},
methods: {
s(sourceCoordinate) { // translate source into target coordinates
let scale;
if (this.boundsC.width < this.boundsC.height) {
scale = this.desiredHeight / this.boundsC.height;
} else {
scale = this.desiredWidth / this.boundsC.width;
}
return sourceCoordinate * scale;
},
rectToStyle(rect) {
const x = this.s(rect.left);
const y = this.s(rect.top);
const w = this.s(rect.right) - this.s(rect.left);
const h = this.s(rect.bottom) - this.s(rect.top);
let t;
if (rect.transform && rect.transform.matrix) {
t = rect.transform.matrix;
} else {
t = rect.transform;
}
const tr = t ? `matrix(${t.dsdx}, ${t.dtdx}, ${t.dsdy}, ${t.dtdy}, ` +
`${this.s(t.tx)}, ${this.s(t.ty)})` : '';
const rectStyle = `top: ${y}px; left: ` +
`${x}px; height: ${h}px; width: ${w}px; ` +
`transform: ${tr}; transform-origin: 0 0;`;
return rectStyle;
},
onClick(rect) {
this.$emit('rect-click', rect.ref);
},
},
};
</script>
<style scoped>
.bounds {
position: relative;
overflow: hidden;
}
.highlight, .rect, .displayRect {
position: absolute;
box-sizing: border-box;
display: flex;
justify-content: flex-end;
}
.rect {
border: 1px solid black;
background-color: rgba(146, 149, 150, 0.8);
}
.highlight {
border: 2px solid rgb(235, 52, 52);
background-color: rgba(243, 212, 212, 0.25);
pointer-events: none;
}
.displayRect {
border: 4px dashed #195aca;
pointer-events: none;
}
.label {
align-self: center;
}
</style>

View File

@@ -1,356 +0,0 @@
<!-- Copyright (C) 2017 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.
-->
<template>
<md-content class="searchbar">
<div class="tabs">
<div class="search-timestamp" v-if="isTimestampSearch()">
<md-field md-inline class="search-input">
<label>Enter timestamp</label>
<md-input
v-model="searchInput"
v-on:focus="updateInputMode(true)"
v-on:blur="updateInputMode(false)"
@keyup.enter.native="updateSearchForTimestamp"
/>
</md-field>
<md-button
class="md-dense md-primary search-timestamp-button"
@click="updateSearchForTimestamp"
>
Go to timestamp
</md-button>
</div>
<div class="dropdown-content" v-if="isTransitionSearch()">
<table>
<tr class="header">
<th style="width: 10%">Global Start</th>
<th style="width: 10%">Global End</th>
<th style="width: 80%">Transition</th>
</tr>
<tr v-for="item in filteredTransitionsAndErrors" :key="item.id">
<td
v-if="isTransition(item)"
class="inline-time"
@click="
setCurrentTimestamp(transitionStart(transitionTags(item.id)))
"
>
<span>{{ transitionTags(item.id)[0].desc }}</span>
</td>
<td
v-if="isTransition(item)"
class="inline-time"
@click="setCurrentTimestamp(transitionEnd(transitionTags(item.id)))"
>
<span>{{ transitionTags(item.id)[1].desc }}</span>
</td>
<td
v-if="isTransition(item)"
class="inline-transition"
:style="{color: transitionTextColor(item.transition)}"
@click="setCurrentTimestamp(transitionStart(transitionTags(item.id)))"
>
{{ transitionDesc(item.transition) }}
</td>
</tr>
</table>
<md-field md-inline class="search-input">
<label>
Filter by transition name. Click to navigate to closest
timestamp in active timeline.
</label>
<md-input
v-model="searchInput"
v-on:focus="updateInputMode(true)"
v-on:blur="updateInputMode(false)"
/>
</md-field>
</div>
<div class="dropdown-content" v-if="isErrorSearch()">
<table>
<tr class="header">
<th style="width: 10%">Timestamp</th>
<th style="width: 90%">Error Message</th>
</tr>
<tr v-for="item in filteredTransitionsAndErrors" :key="item.id">
<td
v-if="!isTransition(item)"
class="inline-time"
@click="setCurrentTimestamp(item.timestamp)"
>
{{ errorDesc(item.timestamp) }}
</td>
<td
v-if="!isTransition(item)"
class="inline-error"
@click="setCurrentTimestamp(item.timestamp)"
>
{{ `${item.assertionName} ${item.message}` }}
</td>
</tr>
</table>
<md-field md-inline class="search-input">
<label>
Filter by error message. Click to navigate to closest
timestamp in active timeline.
</label>
<md-input
v-model="searchInput"
v-on:focus="updateInputMode(true)"
v-on:blur="updateInputMode(false)"
/>
</md-field>
</div>
</div>
<div class="tab-container" v-if="searchTypes.length > 0">
Search mode:
<md-button
v-for="searchType in searchTypes"
:key="searchType"
@click="setSearchType(searchType)"
:class="tabClass(searchType)"
>
{{ searchType }}
</md-button>
</div>
</md-content>
</template>
<script>
import { transitionMap, SEARCH_TYPE } from "./utils/consts";
import { nanos_to_string, getClosestTimestamp } from "./transform";
export default {
name: "searchbar",
props: ["store", "presentTags", "timeline", "presentErrors", "searchTypes"],
data() {
return {
searchType: SEARCH_TYPE.TIMESTAMP,
searchInput: "",
};
},
methods: {
/** Set search type depending on tab selected */
setSearchType(searchType) {
this.searchType = searchType;
},
/** Set tab class to determine color highlight for active tab */
tabClass(searchType) {
var isActive = (this.searchType === searchType) ? 'active' : 'inactive';
return ['tab', isActive];
},
/** Filter all the tags present in the trace by the searchbar input */
filteredTags() {
var tags = [];
var filter = this.searchInput.toUpperCase();
this.presentTags.forEach((tag) => {
const tagTransition = tag.transition.toUpperCase();
if (tagTransition.includes(filter)) tags.push(tag);
});
return tags;
},
/** Add filtered errors to filtered tags to integrate both into table*/
filteredTagsAndErrors() {
var tagsAndErrors = [...this.filteredTags()];
var filter = this.searchInput.toUpperCase();
this.presentErrors.forEach((error) => {
const errorMessage = error.message.toUpperCase();
if (errorMessage.includes(filter)) tagsAndErrors.push(error);
});
// sort into chronological order
tagsAndErrors.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1));
return tagsAndErrors;
},
/** Each transition has two tags present
* Isolate the tags for the desire transition
* Add a desc to display the timestamps as strings
*/
transitionTags(id) {
var tags = this.filteredTags().filter((tag) => tag.id === id);
tags.forEach((tag) => {
tag.desc = nanos_to_string(tag.timestamp);
});
return tags;
},
/** Find the start as minimum timestamp in transition tags */
transitionStart(tags) {
var times = tags.map((tag) => tag.timestamp);
return times[0];
},
/** Find the end as maximum timestamp in transition tags */
transitionEnd(tags) {
var times = tags.map((tag) => tag.timestamp);
return times[times.length - 1];
},
/**
* Upon selecting a start/end tag in the dropdown;
* navigates to that timestamp in the timeline
*/
setCurrentTimestamp(timestamp) {
this.$store.dispatch("updateTimelineTime", timestamp);
},
/** Colour codes text of transition in dropdown */
transitionTextColor(transition) {
return transitionMap.get(transition).color;
},
/** Displays transition description rather than variable name */
transitionDesc(transition) {
return transitionMap.get(transition).desc;
},
/** Add a desc to display the error timestamps as strings */
errorDesc(timestamp) {
return nanos_to_string(timestamp);
},
/** Navigates to closest timestamp in timeline to search input*/
updateSearchForTimestamp() {
const closestTimestamp = getClosestTimestamp(this.searchInput, this.timeline);
this.setCurrentTimestamp(closestTimestamp);
},
isTransitionSearch() {
return this.searchType === SEARCH_TYPE.TRANSITIONS;
},
isErrorSearch() {
return this.searchType === SEARCH_TYPE.ERRORS;
},
isTimestampSearch() {
return this.searchType === SEARCH_TYPE.TIMESTAMP;
},
isTransition(item) {
return item.stacktrace === undefined;
},
/** determines whether left/right arrow keys should move cursor in input field */
updateInputMode(isInputMode) {
this.store.isInputMode = isInputMode;
},
},
computed: {
filteredTransitionsAndErrors() {
var ids = [];
return this.filteredTagsAndErrors().filter((item) => {
if (this.isTransition(item) && !ids.includes(item.id)) {
item.transitionStart = true;
ids.push(item.id);
}
return !this.isTransition(item) || this.isTransition(item) && item.transitionStart;
});
},
},
destroyed() {
this.updateInputMode(false);
},
};
</script>
<style scoped>
.searchbar {
background-color: rgb(250, 243, 233) !important;
top: 0;
left: 0;
right: 0;
width: 100%;
margin-left: auto;
margin-right: auto;
bottom: 1px;
}
.tabs {
padding-top: 1rem;
}
.tab-container {
padding-left: 20px;
display: flex;
align-items: center;
}
.tab.active {
background-color: rgb(236, 222, 202);
}
.tab.inactive {
background-color: rgb(250, 243, 233);
}
.search-timestamp {
padding: 5px 20px 0px 20px;
display: inline-flex;
width: 100%;
}
.search-timestamp > .search-input {
margin-top: -5px;
max-width: 200px;
}
.search-timestamp-button {
left: 0;
padding: 0 15px;
}
.dropdown-content {
padding: 5px 20px 0px 20px;
display: block;
}
.dropdown-content table {
overflow-y: scroll;
max-height: 150px;
display: block;
}
.dropdown-content table td {
padding: 5px;
}
.dropdown-content table th {
text-align: left;
padding: 5px;
}
.inline-time:hover {
background: rgb(216, 250, 218);
cursor: pointer;
}
.inline-transition {
font-weight: bold;
}
.inline-transition:hover {
background: rgb(216, 250, 218);
cursor: pointer;
}
.inline-error {
font-weight: bold;
color: red;
}
.inline-error:hover {
background: rgb(216, 250, 218);
cursor: pointer;
}
</style>

View File

@@ -1,307 +0,0 @@
<!-- 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.
-->
<template>
<div>
<div class="group">
<span class="group-header">Geometry</span>
<div class="left-column">
<div class="column-header">Calculated</div>
<span class="key">Transform:</span>
<TransformMatrix :transform="layer.transform" />
<div />
<span class="key"
>Crop:<md-tooltip
>Raw value read from proto.bounds. This is the buffer size or
requested crop cropped by parent bounds.</md-tooltip
>
</span>
<span class="value">{{ layer.bounds }}</span>
<div />
<span class="key"
>Final Bounds:<md-tooltip
>Raw value read from proto.screenBounds. This is the calculated crop
transformed.</md-tooltip
></span
>
<span class="value">{{ layer.screenBounds }}</span>
</div>
<div class="right-column">
<div class="column-header">Requested</div>
<span class="key">Transform:</span>
<TransformMatrix :transform="layer.requestedTransform" />
<div />
<span class="key">Crop:</span>
<span class="value">{{ layer.crop ? layer.crop : "[empty]" }}</span>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Buffer</span>
</span>
<div v-if="layer.activeBuffer.isNotEmpty" class="left-column">
<div />
<span class="key">Size:</span>
<span class="value">{{ layer.activeBuffer }}</span>
<div />
<span class="key">Frame Number:</span>
<span class="value">{{ layer.currFrame }}</span>
<div />
<span class="key"
>Transform:<md-tooltip
>Rotates or flips the buffer in place. Used with display transform
hint to cancel out any buffer transformation when sending to
HWC.</md-tooltip
></span
>
<span class="value">{{ layer.bufferTransform }}</span>
</div>
<div v-if="layer.activeBuffer.isNotEmpty" class="right-column">
<div />
<span class="key"
>Destination Frame:<md-tooltip
>Scales buffer to the frame by overriding the requested transform
for this layer.</md-tooltip
></span
>
<span class="value">{{ layer.proto.destinationFrame }}</span>
<div />
<span
v-if="(layer.flags & 0x400) /*eIgnoreDestinationFrame*/ === 0x400"
class="value"
>Destination Frame ignored because layer has eIgnoreDestinationFrame
flag set.</span
>
</div>
<div v-if="layer.isContainerLayer" class="left-column">
<span class="key"></span> <span class="value">Container layer</span>
</div>
<div v-if="layer.isEffectLayer" class="left-column">
<span class="key"></span> <span class="value">Effect layer</span>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Hierarchy</span>
</span>
<div class="left-column">
<div />
<span class="key">z-order:</span>
<span class="value">{{ layer.z }}</span>
<div />
<span class="key"
>relative parent:<md-tooltip
>Layer is z-ordered relative to its relative parents but its bounds
and other properties are inherited from its parents.</md-tooltip
></span
>
<span class="value">{{
layer.zOrderRelativeOfId == -1 ? "none" : layer.zOrderRelativeOfId
}}</span>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Effects</span>
</span>
<div class="left-column">
<div class="column-header">Calculated</div>
<span class="key">Color:</span>
<span class="value">{{ layer.color }}</span>
<div />
<span class="key">Shadow:</span>
<span class="value">{{ layer.shadowRadius }} px</span>
<div />
<span class="key">Corner Radius:</span>
<span class="value"
>radius:{{ formatFloat(layer.cornerRadius) }} px</span
>
<div />
<span class="key"
>Corner Radius Crop:<md-tooltip
>Crop used to define the bounds of the corner radii. If the bounds
are greater than the layer bounds then the rounded corner will not
be visible.</md-tooltip
></span
>
<span class="value">{{ layer.cornerRadiusCrop }}</span>
<div />
<span class="key">Blur:</span>
<span class="value"
>{{
layer.proto.backgroundBlurRadius
? layer.proto.backgroundBlurRadius
: 0
}}
px</span
>
</div>
<div class="right-column">
<div class="column-header">Requested</div>
<span class="key">Color:</span>
<span class="value">{{ layer.requestedColor }}</span>
<div />
<span class="key">Shadow:</span>
<span class="value"
>{{
layer.proto.requestedShadowRadius
? layer.proto.requestedShadowRadius
: 0
}}
px</span
>
<div />
<span class="key">Corner Radius:</span>
<span class="value"
>{{
layer.proto.requestedCornerRadius
? formatFloat(layer.proto.requestedCornerRadius)
: 0
}}
px</span
>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Input</span>
</span>
<div v-if="layer.proto.inputWindowInfo" class="left-column">
<span class="key">To Display Transform:</span>
<TransformMatrix :transform="layer.inputTransform" />
<div />
<span class="key">Touchable Region:</span>
<span class="value">{{ layer.inputRegion }}</span>
</div>
<div v-if="layer.proto.inputWindowInfo" class="right-column">
<span class="key">Config:</span>
<span class="value"></span>
<div />
<span class="key">Focusable:</span>
<span class="value">{{ layer.proto.inputWindowInfo.focusable }}</span>
<div />
<span class="key">Crop touch region with layer:</span>
<span class="value">{{
layer.proto.inputWindowInfo.cropLayerId &lt;= 0
? "none"
: layer.proto.inputWindowInfo.cropLayerId
}}</span>
<div />
<span class="key">Replace touch region with crop:</span>
<span class="value">{{
layer.proto.inputWindowInfo.replaceTouchableRegionWithCrop
}}</span>
</div>
<div v-if="!layer.proto.inputWindowInfo" class="left-column">
<span class="key"></span>
<span class="value">No input channel set</span>
</div>
</div>
<div class="group">
<span class="group-header">
<span class="group-heading">Visibility</span>
</span>
<div class="left-column">
<span class="key">Flags:</span>
<span class="value">{{ layer.flags }}</span>
<div />
<div v-if="visibilityReason">
<div v-for="reason in visibilityReason" v-bind:key="reason.key">
<span class="key">{{ reason.key }}:</span>
<span class="value">{{ reason.value }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import TransformMatrix from "@/TransformMatrix.vue";
export default {
name: "SurfaceFlingerPropertyGroups",
props: ["layer", "visibilityReason"],
components: {
TransformMatrix,
},
methods: {
formatFloat(num) {
return Math.round(num * 100) / 100;
},
},
};
</script>
<style scoped>
.group {
padding: 0.5rem;
border-bottom: thin solid rgba(0, 0, 0, 0.12);
flex-direction: row;
display: flex;
}
.group .key {
font-weight: 500;
}
.group .value {
color: rgba(0, 0, 0, 0.75);
}
.group-header {
justify-content: center;
padding: 0px 5px;
width: 80px;
display: inline-block;
font-size: bigger;
color: grey;
}
.left-column {
width: 320px;
max-width: 100%;
display: inline-block;
vertical-align: top;
overflow: auto;
border-right: 5px solid rgba(#000, 0.12);
padding-right: 20px;
}
.right-column {
width: 320px;
max-width: 100%;
display: inline-block;
vertical-align: top;
overflow: auto;
border: 1px solid rgba(#000, 0.12);
}
.column-header {
font-weight: lighter;
font-size: smaller;
}
.element-summary {
padding-top: 1rem;
}
.element-summary .key {
font-weight: 500;
}
.element-summary .value {
color: rgba(0, 0, 0, 0.75);
}
</style>

View File

@@ -1,70 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<TraceView
:store="store"
:file="file"
:summarizer="summarizer"
:presentTags="presentTags"
:presentErrors="presentErrors"
:propertyGroups="true"
/>
</template>
<script>
import TraceView from '@/TraceView.vue';
export default {
name: 'SurfaceFlingerTraceView',
props: ['store', 'file', 'presentTags', 'presentErrors'],
components: {
TraceView,
},
methods: {
summarizer(layer) {
const summary = [];
if (layer?.visibilityReason?.length > 0) {
let reason = "";
if (Array.isArray(layer.visibilityReason)) {
reason = layer.visibilityReason.join(", ");
} else {
reason = layer.visibilityReason;
}
summary.push({key: 'Invisible due to', value: reason});
}
if (layer?.occludedBy?.length > 0) {
summary.push({key: 'Occluded by', value: layer.occludedBy.map(it => it.id).join(', ')});
}
if (layer?.partiallyOccludedBy?.length > 0) {
summary.push({
key: 'Partially occluded by',
value: layer.partiallyOccludedBy.map(it => it.id).join(', '),
});
}
if (layer?.coveredBy?.length > 0) {
summary.push({key: 'Covered by', value: layer.coveredBy.map(it => it.id).join(', ')});
}
return summary;
},
},
};
</script>

View File

@@ -1,146 +0,0 @@
<!-- Copyright (C) 2017 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.
-->
<template>
<div class="timeline-container">
<div class="tag-timeline" v-if="flickerMode" :style="maxOverlap">
<transition-container
class="container"
v-for="transition in timelineTransitions"
:key="transition.type"
:startPos="transition.startPos"
:startTime="transition.startTime"
:endTime="transition.endTime"
:width="transition.width"
:color="transition.color"
:overlap="transition.overlap"
:tooltip="transition.tooltip"
:store="store"
/>
</div>
<svg
width="100%"
height="20"
class="timeline-svg"
:class="{disabled: disabled}"
ref="timeline"
>
<rect
:x="`${block.startPos}%`"
y="0"
:width="`${block.width}%`"
:height="pointHeight"
:rx="corner"
v-for="(block, idx) in timelineBlocks"
:key="idx"
@click="onBlockClick"
class="point"
/>
<rect
:x="`${position(selected)}%`"
y="0"
:width="`${pointWidth}%`"
:height="pointHeight"
:rx="corner"
class="point selected"
/>
<line
v-for="error in errorPositions"
:key="error.pos"
:x1="`${error.pos}%`"
:x2="`${error.pos}%`"
y1="0"
y2="18px"
class="error"
@click="onErrorClick(error.ts)"
/>
</svg>
</div>
</template>
<script>
import TimelineMixin from "./mixins/Timeline.js";
import TransitionContainer from './components/TagDisplay/TransitionContainer.vue';
export default {
name: "timeline",
// TODO: Add indication of trim, at least for collasped timeline
components: {
'transition-container': TransitionContainer,
},
props: ["selectedIndex", "crop", "disabled", "store"],
data() {
return {
pointHeight: 15,
corner: 2
};
},
mixins: [TimelineMixin],
computed: {
timestamps() {
if (this.timeline.length == 1) {
return [0];
}
return this.timeline;
},
selected() {
return this.timeline[this.selectedIndex];
},
maxOverlap() {
if (!this.timelineTransitions) {
return {
marginTop: '0px',
}
}
var overlaps = [];
for (const transition in this.timelineTransitions) {
overlaps.push(this.timelineTransitions[transition].overlap);
}
return {
marginTop: (Math.max(...overlaps)+1)*10 + 'px',
}
},
}
};
</script>
<style scoped>
.timeline-container {
width: 100%;
}
.container:hover {
cursor: pointer;
}
.tag-timeline {
width: 100%;
position: relative;
height: 10px;
}
.timeline-svg .point {
cursor: pointer;
}
.timeline-svg.disabled .point {
fill: #BDBDBD;
cursor: not-allowed;
}
.timeline-svg:not(.disabled) .point.selected {
fill: #b2f6faff;
}
.timeline-svg.disabled .point.selected {
fill: rgba(240, 59, 59, 0.596);
}
.error {
stroke: rgb(255, 0, 0);
stroke-width: 8px;
cursor: pointer;
}
</style>

View File

@@ -1,463 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<div class="wrapper">
<svg
width="100%"
height="20"
class="timeline-svg"
:class="{disabled: disabled}"
ref="timeline"
>
<rect
:x="`${block.startPos}%`"
y="0"
:width="`${block.width}%`"
:height="pointHeight"
:rx="corner"
v-for="(block, idx) in timelineBlocks"
:key="idx"
class="point"
/>
<rect
v-if="selectedWidth >= 0"
v-show="showSelection"
:x="selectionAreaStart"
y="0"
:width="selectedWidth"
:height="pointHeight"
:rx="corner"
class="point selection"
ref="selectedSection"
/>
<rect
v-else
v-show="showSelection"
:x="selectionAreaEnd"
y="0"
:width="-selectedWidth"
:height="pointHeight"
:rx="corner"
class="point selection"
ref="selectedSection"
/>
<rect
v-show="showSelection"
:x="selectionAreaStart - 2"
y="0"
:width="4"
:height="pointHeight"
:rx="corner"
class="point selection-edge"
ref="leftResizeDragger"
/>
<rect
v-show="showSelection"
:x="selectionAreaEnd - 2"
y="0"
:width="4"
:height="pointHeight"
:rx="corner"
class="point selection-edge"
ref="rightResizeDragger"
/>
</svg>
</div>
</template>
<script>
import TimelineMixin from './mixins/Timeline';
export default {
name: 'timelineSelection',
props: ['startTimestamp', 'endTimestamp', 'cropArea', 'disabled'],
data() {
return {
pointHeight: 15,
corner: 2,
selectionStartPosition: 0,
selectionEndPosition: 0,
selecting: false,
dragged: false,
draggingSelection: false,
};
},
mixins: [TimelineMixin],
watch: {
selectionStartPosition() {
// Send crop intent rather than final crop value while we are selecting
if ((this.selecting && this.dragged)) {
this.emitCropIntent();
return;
}
this.emitCropDetails();
},
selectionEndPosition() {
// Send crop intent rather than final crop value while we are selecting
if ((this.selecting && this.dragged)) {
this.emitCropIntent();
return;
}
this.emitCropDetails();
},
},
methods: {
/**
* Create an object that can be injected and removed from the DOM to change
* the cursor style. The object is a mask over the entire screen. It is
* done this way as opposed to injecting a style targeting all elements for
* performance reasons, otherwise recalculate style would be very slow.
* This makes sure that regardless of the cursor style of other elements,
* the cursor style will be set to what we want over the entire screen.
* @param {string} cursor - The cursor type to apply to the entire page.
* @return An object that can be injected and removed from the DOM which
* changes the cursor style for the entire page.
*/
createCursorStyle(cursor) {
const cursorMask = document.createElement('div');
cursorMask.style.cursor = cursor;
cursorMask.style.height = '100vh';
cursorMask.style.width = '100vw';
cursorMask.style.position = 'fixed';
cursorMask.style.top = '0';
cursorMask.style.left = '0';
cursorMask.style['z-index'] = '10';
return {
inject: () => {
document.body.appendChild(cursorMask);
},
remove: () => {
try {
document.body.removeChild(cursorMask);
} catch (e) {}
},
};
},
setupCreateSelectionListeners() {
const cursorStyle = this.createCursorStyle('crosshair');
this.timelineSvgMouseDownEventListener = (e) => {
e.stopPropagation();
this.selecting = true;
this.dragged = false;
this.mouseDownX = e.offsetX;
this.mouseDownClientX = e.clientX;
cursorStyle.inject();
};
this.createSelectionMouseMoveEventListener = (e) => {
if (this.selecting) {
if (!this.dragged) {
this.selectionStartX = this.mouseDownX;
}
this.dragged = true;
const draggedAmount = e.clientX - this.mouseDownClientX;
if (draggedAmount >= 0) {
this.selectionStartPosition = this.selectionStartX;
const endX = this.selectionStartX + draggedAmount;
if (endX <= this.$refs.timeline.clientWidth) {
this.selectionEndPosition = endX;
} else {
this.selectionEndPosition = this.$refs.timeline.clientWidth;
}
this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionEndPosition));
} else {
this.selectionEndPosition = this.selectionStartX;
const startX = this.selectionStartX + draggedAmount;
if (startX >= 0) {
this.selectionStartPosition = startX;
} else {
this.selectionStartPosition = 0;
}
this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionStartPosition));
}
}
};
this.createSelectionMouseUpEventListener = (e) => {
this.selecting = false;
cursorStyle.remove();
this.$emit('resetVideoTimestamp');
if (this.dragged) {
// Clear crop intent, we now have a set crop value
this.clearCropIntent();
// Notify of final crop value
this.emitCropDetails();
}
this.dragged = false;
};
this.$refs.timeline
.addEventListener('mousedown', this.timelineSvgMouseDownEventListener);
document
.addEventListener('mousemove', this.createSelectionMouseMoveEventListener);
document
.addEventListener('mouseup', this.createSelectionMouseUpEventListener);
},
teardownCreateSelectionListeners() {
this.$refs.timeline
.removeEventListener('mousedown', this.timelineSvgMouseDownEventListener);
document
.removeEventListener('mousemove', this.createSelectionMouseMoveEventListener);
document
.removeEventListener('mouseup', this.createSelectionMouseUpEventListener);
},
setupDragSelectionListeners() {
const cursorStyle = this.createCursorStyle('move');
this.selectedSectionMouseDownListener = (e) => {
e.stopPropagation();
this.draggingSelectionStartX = e.clientX;
this.selectionStartPosition = this.selectionAreaStart;
this.selectionEndPosition = this.selectionAreaEnd;
this.draggingSelectionStartPos = this.selectionAreaStart;
this.draggingSelectionEndPos = this.selectionAreaEnd;
// Keep this after fetching selectionAreaStart and selectionAreaEnd.
this.draggingSelection = true;
cursorStyle.inject();
};
this.dragSelectionMouseMoveEventListener = (e) => {
if (this.draggingSelection) {
const dragAmount = e.clientX - this.draggingSelectionStartX;
const newStartPos = this.draggingSelectionStartPos + dragAmount;
const newEndPos = this.draggingSelectionEndPos + dragAmount;
if (newStartPos >= 0 && newEndPos <= this.$refs.timeline.clientWidth) {
this.selectionStartPosition = newStartPos;
this.selectionEndPosition = newEndPos;
} else {
if (newStartPos < 0) {
this.selectionStartPosition = 0;
this.selectionEndPosition = newEndPos - (newStartPos /* negative overflown amount*/);
} else {
const overflownAmount = newEndPos - this.$refs.timeline.clientWidth;
this.selectionEndPosition = this.$refs.timeline.clientWidth;
this.selectionStartPosition = newStartPos - overflownAmount;
}
}
}
};
this.dragSelectionMouseUpEventListener = (e) => {
this.draggingSelection = false;
cursorStyle.remove();
};
this.$refs.selectedSection
.addEventListener('mousedown', this.selectedSectionMouseDownListener);
document
.addEventListener('mousemove', this.dragSelectionMouseMoveEventListener);
document
.addEventListener('mouseup', this.dragSelectionMouseUpEventListener);
},
teardownDragSelectionListeners() {
this.$refs.selectedSection
.removeEventListener('mousedown', this.selectedSectionMouseDownListener);
document
.removeEventListener('mousemove', this.dragSelectionMouseMoveEventListener);
document
.removeEventListener('mouseup', this.dragSelectionMouseUpEventListener);
},
setupResizeSelectionListeners() {
const cursorStyle = this.createCursorStyle('ew-resize');
this.leftResizeDraggerMouseDownEventListener = (e) => {
e.stopPropagation();
this.resizeStartX = e.clientX;
this.selectionStartPosition = this.selectionAreaStart;
this.selectionEndPosition = this.selectionAreaEnd;
this.resizeStartPos = this.selectionAreaStart;
this.resizeingLeft = true;
cursorStyle.inject();
this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionAreaStart));
};
this.rightResizeDraggerMouseDownEventListener = (e) => {
e.stopPropagation();
this.resizeStartX = e.clientX;
this.selectionStartPosition = this.selectionAreaStart;
this.selectionEndPosition = this.selectionAreaEnd;
this.resizeEndPos = this.selectionAreaEnd;
this.resizeingRight = true;
cursorStyle.inject();
this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionAreaEnd));
};
this.resizeMouseMoveEventListener = (e) => {
if (this.resizeingLeft) {
const moveAmount = e.clientX - this.resizeStartX;
let newStartPos = this.resizeStartPos + moveAmount;
if (newStartPos >= this.selectionEndPosition) {
newStartPos = this.selectionEndPosition;
}
if (newStartPos < 0) {
newStartPos = 0;
}
this.selectionStartPosition = newStartPos;
this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionStartPosition));
}
if (this.resizeingRight) {
const moveAmount = e.clientX - this.resizeStartX;
let newEndPos = this.resizeEndPos + moveAmount;
if (newEndPos <= this.selectionStartPosition) {
newEndPos = this.selectionStartPosition;
}
if (newEndPos > this.$refs.timeline.clientWidth) {
newEndPos = this.$refs.timeline.clientWidth;
}
this.selectionEndPosition = newEndPos;
this.$emit('showVideoAt', this.absolutePositionAsTimestamp(this.selectionEndPosition));
}
};
this.resizeSelectionMouseUpEventListener = (e) => {
this.resizeingLeft = false;
this.resizeingRight = false;
cursorStyle.remove();
this.$emit('resetVideoTimestamp');
};
this.$refs.leftResizeDragger
.addEventListener('mousedown', this.leftResizeDraggerMouseDownEventListener);
this.$refs.rightResizeDragger
.addEventListener('mousedown', this.rightResizeDraggerMouseDownEventListener);
document
.addEventListener('mousemove', this.resizeMouseMoveEventListener);
document
.addEventListener('mouseup', this.resizeSelectionMouseUpEventListener);
},
teardownResizeSelectionListeners() {
this.$refs.leftResizeDragger
.removeEventListener('mousedown', this.leftResizeDraggerMouseDownEventListener);
this.$refs.rightResizeDragger
.removeEventListener('mousedown', this.rightResizeDraggerMouseDownEventListener);
document
.removeEventListener('mousemove', this.resizeMouseMoveEventListener);
document
.removeEventListener('mouseup', this.resizeSelectionMouseUpEventListener);
},
emitCropDetails() {
const width = this.$refs.timeline.clientWidth;
this.$emit('crop', {
left: this.selectionStartPosition / width,
right: this.selectionEndPosition / width,
});
},
emitCropIntent() {
const width = this.$refs.timeline.clientWidth;
this.$emit('cropIntent', {
left: this.selectionStartPosition / width,
right: this.selectionEndPosition / width
});
},
clearCropIntent() {
this.$emit('cropIntent', null);
}
},
computed: {
selected() {
return this.timeline[this.selectedIndex];
},
selectedWidth() {
return this.selectionAreaEnd - this.selectionAreaStart;
},
showSelection() {
return this.selectionAreaStart || this.selectionAreaEnd;
},
selectionAreaStart() {
if ((this.selecting && this.dragged) || this.draggingSelection) {
return this.selectionStartPosition;
}
if (this.cropArea && this.$refs.timeline) {
return this.cropArea.left * this.$refs.timeline.clientWidth;
}
return 0;
},
selectionAreaEnd() {
if ((this.selecting && this.dragged) || this.draggingSelection) {
return this.selectionEndPosition;
}
if (this.cropArea && this.$refs.timeline) {
return this.cropArea.right * this.$refs.timeline.clientWidth;
}
return 0;
},
},
mounted() {
this.setupCreateSelectionListeners();
this.setupDragSelectionListeners();
this.setupResizeSelectionListeners();
},
beforeDestroy() {
this.teardownCreateSelectionListeners();
this.teardownDragSelectionListeners();
this.teardownResizeSelectionListeners();
},
};
</script>
<style scoped>
.wrapper {
padding: 0 15px;
}
.timeline-svg {
cursor: crosshair;
}
.timeline-svg .point {
fill: #BDBDBD;
}
.timeline-svg .point.selection {
fill: rgba(240, 59, 59, 0.596);
cursor: move;
}
.timeline-svg .point.selection-edge {
fill: rgba(27, 123, 212, 0.596);
cursor: ew-resize;
}
</style>

View File

@@ -1,385 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<div class="timelines-container">
<div class="timeline-icons" @mousedown="mousedownHandler">
<div
v-for="file in timelineFiles"
:key="file.filename"
class="trace-icon"
:class="{disabled: file.timelineDisabled}"
@click="toggleTimeline(file)"
style="cursor: pointer;"
>
<i class="material-icons">
{{ TRACE_ICONS[file.type] }}
<md-tooltip md-direction="bottom">{{ file.type }}</md-tooltip>
</i>
</div>
</div>
<div class="timelines-wrapper" ref="timelinesWrapper">
<md-list class="timelines" @mousedown="mousedownHandler" ref="timelines">
<md-list-item
v-for="file in timelineFiles"
:key="file.filename"
>
<timeline
:timeline="Object.freeze(file.timeline)"
:selected-index="file.selectedIndex"
:scale="scale"
:crop="crop"
:disabled="file.timelineDisabled"
class="timeline"
/>
</md-list-item>
</md-list>
<div
class="selection"
:style="selectionStyle"
/>
<div
v-show="this.cropIntent"
class="selection-intent"
:style="selectionIntentStyle"
/>
</div>
</div>
</template>
<script>
import Timeline from './Timeline.vue';
import {TRACE_ICONS} from '@/decode.js';
export default {
name: 'Timelines',
props: ['timelineFiles', 'scale', 'crop', 'cropIntent'],
data() {
return {
// Distances of sides from top left corner of wrapping div in pixels
selectionPosition: {
top: 0,
left: 0,
bottom: 0,
right: 0,
},
TRACE_ICONS,
};
},
computed: {
/**
* Used to check whether or not a selection box should be displayed.
* @return {bool} true if any of the positions are non nullish values
*/
isEmptySelection() {
return this.selectionPosition.top ||
this.selectionPosition.left ||
this.selectionPosition.bottom ||
this.selectionPosition.right;
},
/**
* Generates the style of the selection box.
* @return {object} an object containing the style of the selection box.
*/
selectionStyle() {
return {
top: `${this.selectionPosition.top}px`,
left: `${this.selectionPosition.left}px`,
height:
`${this.selectionPosition.bottom - this.selectionPosition.top}px`,
width:
`${this.selectionPosition.right - this.selectionPosition.left}px`,
};
},
/**
* Generates the dynamic style of the selection intent box.
* @return {object} an object containing the style of the selection intent
* box.
*/
selectionIntentStyle() {
if (!(this.cropIntent && this.$refs.timelinesWrapper)) {
return {
left: 0,
width: 0,
};
}
const activeCropLeft = this.crop?.left ?? 0;
const activeCropRight = this.crop?.right ?? 1;
const timelineWidth =
this.$refs.timelinesWrapper.getBoundingClientRect().width;
const r = timelineWidth / (activeCropRight - activeCropLeft);
let left = 0;
let boderLeft = 'none';
if (this.cropIntent.left > activeCropLeft) {
left = (this.cropIntent.left - activeCropLeft) * r;
boderLeft = null;
}
let right = timelineWidth;
let borderRight = 'none';
if (this.cropIntent.right < activeCropRight) {
right = timelineWidth - (activeCropRight - this.cropIntent.right) * r;
borderRight = null;
}
return {
'left': `${left}px`,
'width': `${right - left}px`,
'border-left': boderLeft,
'border-right': borderRight,
};
},
},
methods: {
/**
* Adds an overlay to make sure element selection can't happen and the
* crosshair cursor style is maintained wherever the curso is on the screen
* while a selection is taking place.
*/
addOverlay() {
if (this.overlay) {
return;
}
this.overlay = document.createElement('div');
Object.assign(this.overlay.style, {
'position': 'fixed',
'top': 0,
'left': 0,
'height': '100vh',
'width': '100vw',
'z-index': 10,
'cursor': 'crosshair',
});
document.body.appendChild(this.overlay);
},
/**
* Removes the overlay that is added by a call to addOverlay.
*/
removeOverlay() {
if (!this.overlay) {
return;
}
document.body.removeChild(this.overlay);
delete this.overlay;
},
/**
* Generates an object that can is used to update the position and style of
* the selection box when a selection is being made. The object contains
* three functions which all take a DOM event as a parameter.
*
* - init: setup the initial drag position of the selection base on the
* mousedown event
* - update: updates the selection box's coordinates based on the mousemouve
* event
* - reset: clears the selection box, shold be called when the mouseup event
* occurs or when we want to no longer display the selection box.
* @return {null}
*/
selectionPositionsUpdater() {
let startClientX; let startClientY; let x; let y;
return {
init: (e) => {
startClientX = e.clientX;
startClientY = e.clientY;
x = startClientX -
this.$refs.timelines.$el.getBoundingClientRect().left;
y = startClientY -
this.$refs.timelines.$el.getBoundingClientRect().top;
},
update: (e) => {
let left; let right; let top; let bottom;
const xDiff = e.clientX - startClientX;
if (xDiff > 0) {
left = x;
right = x + xDiff;
} else {
left = x + xDiff;
right = x;
}
const yDiff = e.clientY - startClientY;
if (yDiff > 0) {
top = y;
bottom = y + yDiff;
} else {
top = y + yDiff;
bottom = y;
}
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
if (right > this.$refs.timelines.$el.getBoundingClientRect().width) {
right = this.$refs.timelines.$el.getBoundingClientRect().width;
}
if (bottom >
this.$refs.timelines.$el.getBoundingClientRect().height) {
bottom = this.$refs.timelines.$el.getBoundingClientRect().height;
}
this.$set(this.selectionPosition, 'left', left);
this.$set(this.selectionPosition, 'right', right);
this.$set(this.selectionPosition, 'top', top);
this.$set(this.selectionPosition, 'bottom', bottom);
},
reset: (e) => {
this.$set(this.selectionPosition, 'left', 0);
this.$set(this.selectionPosition, 'right', 0);
this.$set(this.selectionPosition, 'top', 0);
this.$set(this.selectionPosition, 'bottom', 0);
},
};
},
/**
* Handles the mousedown event indicating the start of a selection.
* Adds listeners to handles mousemove and mouseup event to detect the
* selection and update the selection box's coordinates.
* @param {event} e
*/
mousedownHandler(e) {
const selectionPositionsUpdater = this.selectionPositionsUpdater();
const startClientX = e.clientX;
const startClientY = e.clientY;
selectionPositionsUpdater.init(e);
let dragged = false;
const mousemoveHandler = (e) => {
if (!dragged) {
const xDiff = e.clientX - startClientX;
const yDiff = e.clientY - startClientY;
// Only start the cropping procedures and show the cropping overlay
// once we have dragged more than 5 pixels to mark a deliberate drag
// rather than an accidental subtle movement when clicking on an entry
if (xDiff > 10 || yDiff > 10) {
dragged = true;
this.addOverlay();
}
}
selectionPositionsUpdater.update(e);
};
document.addEventListener('mousemove', mousemoveHandler);
const mouseupHandler = (e) => {
document.removeEventListener('mousemove', mousemoveHandler);
document.removeEventListener('mouseup', mouseupHandler);
if (dragged) {
this.removeOverlay();
selectionPositionsUpdater.update(e);
this.zoomToSelection();
}
selectionPositionsUpdater.reset();
};
document.addEventListener('mouseup', mouseupHandler);
},
/**
* Update the crop values to zoom into the timeline based on the currently
* set selection box coordinates.
*/
zoomToSelection() {
const left = this.crop?.left ?? 0;
const right = this.crop?.right ?? 1;
const ratio =
(this.selectionPosition.right - this.selectionPosition.left) /
this.$refs.timelines.$el.getBoundingClientRect().width;
const newCropWidth = ratio * (right - left);
const newLeft = left + (this.selectionPosition.left /
this.$refs.timelines.$el.getBoundingClientRect().width) *
(right - left);
if (this.crop) {
this.$set(this.crop, 'left', newLeft);
this.$set(this.crop, 'right', newLeft + newCropWidth);
} else {
this.$emit('crop', {
left: newLeft,
right: newLeft + newCropWidth,
});
}
},
toggleTimeline(file) {
this.$set(file, 'timelineDisabled', !file.timelineDisabled);
},
},
components: {
Timeline,
},
};
</script>
<style scoped>
.timelines-container {
display: flex;
}
.timelines-container .timelines-wrapper {
flex-grow: 1;
cursor: crosshair;
position: relative;
}
.timelines-wrapper {
overflow: hidden;
}
.selection, .selection-intent {
position: absolute;
z-index: 10;
background: rgba(255, 36, 36, 0.5);
pointer-events: none;
}
.selection-intent {
top: 0;
height: 100%;
margin-left: -3px;
border-left: 3px #1261A0 solid;
border-right: 3px #1261A0 solid;
}
.timeline-icons {
display: flex;
flex-direction: column;
justify-content: space-evenly;
margin-left: 15px;
}
.trace-icon.disabled {
color: gray;
}
</style>

View File

@@ -1,31 +0,0 @@
<script>
export default {
name: 'vue-title',
props: ['appName', 'traceName'],
watch: {
traceName: {
immediate: true,
handler() {
this.updatePageTitle();
}
},
appName: {
immediate: true,
handler() {
this.updatePageTitle();
}
}
},
methods: {
updatePageTitle() {
if (this.traceName == null || this.traceName == "") {
document.title = this.appName;
} else {
document.title = this.traceName;
}
}
},
render () {
},
}
</script>

View File

@@ -1,639 +0,0 @@
<!-- Copyright (C) 2019 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.
-->
<template>
<md-card-content class="container">
<div class="rects" v-if="hasScreenView">
<rects
:bounds="bounds"
:rects="rects"
:displays="displays"
:highlight="highlight"
@rect-click="onRectClick"
/>
</div>
<div class="hierarchy">
<flat-card
v-bind:class ="imeAdditionalProperties ? 'height-reduced' : ''">
<md-content
md-tag="md-toolbar"
md-elevation="0"
class="card-toolbar md-transparent md-dense"
>
<h2 class="md-title" style="flex: 1;">Hierarchy</h2>
<md-checkbox
v-model="showHierarchyDiff"
v-if="diffVisualizationAvailable"
>
Show Diff
</md-checkbox>
<md-checkbox v-model="store.simplifyNames">
Simplify names
</md-checkbox>
<md-checkbox v-model="store.onlyVisible">Only visible</md-checkbox>
<md-checkbox v-model="store.flattened">Flat</md-checkbox>
<md-checkbox v-if="hasTagsOrErrors" v-model="store.flickerTraceView">
Flicker</md-checkbox>
<md-field md-inline class="filter">
<label>Filter...</label>
<md-input
v-model="hierarchyPropertyFilterString"
v-on:focus="updateInputMode(true)"
v-on:blur="updateInputMode(false)"
/>
</md-field>
</md-content>
<div class="hierarchy-content">
<properties-table-view
v-if="propertiesForTableView"
:tableEntries="propertiesForTableView"
/>
<div class="tree-view-wrapper">
<tree-view
class="treeview"
:item="tree"
@item-selected="itemSelected"
:selected="hierarchySelected"
:filter="hierarchyFilter"
:flattened="store.flattened"
:onlyVisible="store.onlyVisible"
:flickerTraceView="store.flickerTraceView"
:presentTags="presentTags"
:presentErrors="presentErrors"
:items-clickable="true"
:useGlobalCollapsedState="true"
:simplify-names="store.simplifyNames"
ref="hierarchy"
/>
</div>
</div>
</flat-card>
<div v-if="imeAdditionalProperties" class="ime-additional-properties">
<flat-card>
<md-content
md-tag="md-toolbar"
md-elevation="0"
class="card-toolbar md-transparent md-dense"
>
<h2 class="md-title" style="flex: 1;">WM & SF Properties</h2>
</md-content>
<div>
<ime-additional-properties
:entry="this.item"
:isImeManagerService="this.isImeManagerService"
:onSelectItem="itemSelected"
/>
</div>
</flat-card>
</div>
</div>
<div class="properties">
<flat-card>
<md-content
md-tag="md-toolbar"
md-elevation="0"
class="card-toolbar md-transparent md-dense"
>
<h2 class="md-title" style="flex: 1">Properties</h2>
<div>
<md-checkbox
v-model="displayDefaults"
@change="checkboxChange"
>
Show Defaults
</md-checkbox>
<md-tooltip md-direction="bottom">
If checked, shows the value of all properties.
Otherwise, hides all properties whose value is
the default for its data type.
</md-tooltip>
</div>
<md-checkbox
v-model="showPropertiesDiff"
v-if="diffVisualizationAvailable"
>
Show Diff
</md-checkbox>
<md-field md-inline class="filter">
<label>Filter...</label>
<md-input
v-model="propertyFilterString"
v-on:focus="updateInputMode(true)"
v-on:blur="updateInputMode(false)"
/>
</md-field>
</md-content>
<div class="properties-content">
<div v-if="elementSummary && !propertyGroups" class="element-summary">
<div v-for="elem in elementSummary" v-bind:key="elem.key">
<span class="key">{{ elem.key }}:</span>
<span class="value">{{ elem.value }}</span>
</div>
</div>
<div v-if="selectedTree && propertyGroups" class="element-summary">
<sf-property-groups
:layer="this.hierarchySelected"
:visibilityReason="elementSummary"
/>
</div>
<div v-if="selectedTree" class="tree-view-wrapper">
<tree-view
class="treeview"
:item="selectedTree"
:filter="propertyFilter"
:collapseChildren="true"
:elementView="PropertiesTreeElement"
/>
</div>
<div class="no-properties" v-else>
<i class="material-icons none-icon">
filter_none
</i>
<span>No element selected in the hierarchy.</span>
</div>
</div>
</flat-card>
</div>
</md-card-content>
</template>
<script>
import TreeView from './TreeView.vue';
import Rects from './Rects.vue';
import FlatCard from './components/FlatCard.vue';
import PropertiesTreeElement from './PropertiesTreeElement.vue';
import SurfaceFlingerPropertyGroups from '@/SurfaceFlingerPropertyGroups.vue';
import PropertiesTableView from './PropertiesTableView';
import ImeAdditionalProperties from '@/ImeAdditionalProperties';
import {ObjectTransformer} from './transform.js';
import {DiffGenerator, defaultModifiedCheck} from './utils/diff.js';
import {TRACE_TYPES, DUMP_TYPES} from './decode.js';
import {
isPropertyMatch, stableIdCompatibilityFixup, getFilter,
} from './utils/utils.js';
import {CompatibleFeatures} from './utils/compatibility.js';
import {getPropertiesForDisplay} from './flickerlib/mixin';
import ObjectFormatter from './flickerlib/ObjectFormatter';
function formatProto(obj) {
if (obj?.prettyPrint) {
return obj.prettyPrint();
}
}
function findEntryInTree(tree, id) {
if (tree.stableId === id) {
return tree;
}
if (!tree.children) {
return null;
}
for (const child of tree.children) {
const foundEntry = findEntryInTree(child, id);
if (foundEntry) {
return foundEntry;
}
}
return null;
}
export default {
name: 'traceview',
props: ['store', 'file', 'summarizer', 'presentTags', 'presentErrors',
'propertyGroups', 'imeAdditionalProperties'],
data() {
return {
propertyFilterString: '',
hierarchyPropertyFilterString: '',
selectedTree: null,
hierarchySelected: null,
lastSelectedStableId: null,
bounds: {},
rects: [],
displays: [],
item: null,
tree: null,
highlight: null,
showHierarchyDiff: false,
displayDefaults: false,
showPropertiesDiff: false,
PropertiesTreeElement,
isImeManagerService: false,
};
},
methods: {
checkboxChange(checked) {
this.itemSelected(this.item);
},
itemSelected(item) {
this.hierarchySelected = item;
this.selectedTree = this.getTransformedProperties(item);
this.highlight = item.rect;
this.lastSelectedStableId = item.stableId;
// Record analytics event
if (item.type || item.kind || item.stableId) {
this.recordOpenedEntryEvent(item.type ?? item.kind ?? item.stableId);
}
this.$emit('focus');
},
getTransformedProperties(item) {
ObjectFormatter.displayDefaults = this.displayDefaults;
// There are 2 types of object whose properties can appear in the property
// list: Flicker objects (WM/SF traces) and dictionaries
// (IME/Accessibilty/Transactions).
// While flicker objects have their properties directly in the main
// object, those created by a call to the transform function have their
// properties inside an obj property. This makes both cases work
// TODO(209452852) Refactor both flicker and winscope-native objects to
// implement a common display interface that can be better handled
const target = item.obj ?? item;
const transformer = new ObjectTransformer(
getPropertiesForDisplay(target),
item.name,
stableIdCompatibilityFixup(item),
).setOptions({
skip: item.skip,
formatter: formatProto,
});
if (this.showPropertiesDiff && this.diffVisualizationAvailable) {
const prevItem = this.getItemFromPrevTree(item);
transformer.withDiff(getPropertiesForDisplay(prevItem));
}
return transformer.transform();
},
onRectClick(item) {
if (item) {
this.itemSelected(item);
}
},
generateTreeFromItem(item) {
if (!this.showHierarchyDiff || !this.diffVisualizationAvailable) {
return item;
}
const thisItem = this.item;
const prevItem = this.getDataWithOffset(-1);
return new DiffGenerator(thisItem)
.compareWith(prevItem)
.withUniqueNodeId((node) => {
return node.stableId;
})
.withModifiedCheck(defaultModifiedCheck)
.generateDiffTree();
},
setData(item) {
this.item = item;
this.tree = this.generateTreeFromItem(item);
const rects = item.rects; // .toArray()
this.rects = [...rects].reverse();
this.bounds = item.bounds;
// only update displays if item is SF trace and displays present
if (item.stableId==='LayerTraceEntry') {
this.displays = item.displays;
} else {
this.displays = [];
}
this.hierarchySelected = null;
this.selectedTree = null;
this.highlight = null;
function findItem(item, stableId) {
if (item.stableId === stableId) {
return item;
}
if (Array.isArray(item.children)) {
for (const child of item.children) {
const found = findItem(child, stableId);
if (found) {
return found;
}
}
}
return null;
}
if (this.lastSelectedStableId) {
const found = findItem(item, this.lastSelectedStableId);
if (found) {
this.itemSelected(found);
}
}
this.isImeManagerService =
this.file.type === TRACE_TYPES.IME_MANAGERSERVICE;
},
arrowUp() {
return this.$refs.hierarchy.selectPrev();
},
arrowDown() {
return this.$refs.hierarchy.selectNext();
},
getDataWithOffset(offset) {
const index = this.file.selectedIndex + offset;
if (index < 0 || index >= this.file.data.length) {
return null;
}
return this.file.data[index];
},
getItemFromPrevTree(entry) {
if (!this.showPropertiesDiff || !this.hierarchySelected) {
return null;
}
const id = entry.stableId;
if (!id) {
throw new Error('Entry has no stableId...');
}
const prevTree = this.getDataWithOffset(-1);
if (!prevTree) {
console.warn('No previous entry');
return null;
}
const prevEntry = findEntryInTree(prevTree, id);
if (!prevEntry) {
console.warn('Didn\'t exist in last entry');
// TODO: Maybe handle this in some way.
}
return prevEntry;
},
/** Performs check for id match between entry and present tags/errors
* must be carried out for every present tag/error
*/
matchItems(flickerItems, entryItem) {
let match = false;
flickerItems.forEach((flickerItem) => {
if (isPropertyMatch(flickerItem, entryItem)) match = true;
});
return match;
},
/** Returns check for id match between entry and present tags/errors */
isEntryTagMatch(entryItem) {
return this.matchItems(this.presentTags, entryItem) ||
this.matchItems(this.presentErrors, entryItem);
},
/** determines whether left/right arrow keys should move cursor in input field */
updateInputMode(isInputMode) {
this.store.isInputMode = isInputMode;
},
},
created() {
const item = this.file.data[this.file.selectedIndex ?? 0];
if (item) {
// Record analytics event
if (item.type || item.kind || item.stableId) {
this.recordOpenTraceEvent(item.type ?? item.kind ?? item.stableId);
}
this.setData(item);
} else {
console.log('Item passed into TraceView is null or undefined: ', item);
}
},
destroyed() {
this.store.flickerTraceView = false;
},
watch: {
selectedIndex() {
this.setData(this.file.data[this.file.selectedIndex ?? 0]);
},
showHierarchyDiff() {
this.tree = this.generateTreeFromItem(this.item);
},
showPropertiesDiff() {
if (this.hierarchySelected) {
this.selectedTree =
this.getTransformedProperties(this.hierarchySelected);
}
},
},
computed: {
diffVisualizationAvailable() {
return CompatibleFeatures.DiffVisualization && (
this.file.type == TRACE_TYPES.WINDOW_MANAGER ||
this.file.type == TRACE_TYPES.SURFACE_FLINGER
);
},
selectedIndex() {
return this.file.selectedIndex;
},
hierarchyFilter() {
const hierarchyPropertyFilter =
getFilter(this.hierarchyPropertyFilterString);
const fil = this.store.onlyVisible ? (c) => {
return c.isVisible && hierarchyPropertyFilter(c);
} : hierarchyPropertyFilter;
return this.store.flickerTraceView ? (c) => {
return this.isEntryTagMatch(c);
} : fil;
},
propertyFilter() {
return getFilter(this.propertyFilterString);
},
hasScreenView() {
return this.file.type == TRACE_TYPES.WINDOW_MANAGER ||
this.file.type == TRACE_TYPES.SURFACE_FLINGER ||
this.file.type == DUMP_TYPES.WINDOW_MANAGER ||
this.file.type == DUMP_TYPES.SURFACE_FLINGER;
},
elementSummary() {
if (!this.hierarchySelected || !this.summarizer) {
return null;
}
const summary = this.summarizer(this.hierarchySelected);
if (summary?.length === 0) {
return null;
}
return summary;
},
hasTagsOrErrors() {
return this.presentTags.length > 0 || this.presentErrors.length > 0;
},
propertiesForTableView() {
if (this.file.type == TRACE_TYPES.IME_CLIENTS) {
if (!this.item?.obj?.client) {
console.log('ImeTrace Clients: Client is null');
}
return {
'inputMethodId': this.item?.obj?.client?.inputMethodManager?.curId,
'packageName': this.item?.obj?.client?.editorInfo?.packageName,
};
} else if (this.file.type == TRACE_TYPES.IME_SERVICE) {
if (!this.item?.obj?.inputMethodService) {
console.log('ImeTrace InputMethodService: ' +
'inputMethodService is null');
}
return {
'windowVisible': this.item?.obj?.inputMethodService?.windowVisible,
'decorViewVisible':
this.item?.obj?.inputMethodService?.decorViewVisible,
'packageName':
this.item?.obj?.inputMethodService?.inputEditorInfo?.packageName,
};
} else if (this.file.type == TRACE_TYPES.IME_MANAGERSERVICE) {
if (!this.item?.obj?.inputMethodManagerService) {
console.log('ImeTrace InputMethodManagerService: ' +
'inputMethodManagerService is null');
}
return {
'inputMethodId':
this.item?.obj?.inputMethodManagerService?.curMethodId,
'curFocusedWindow':
this.item?.obj?.inputMethodManagerService?.curFocusedWindowName,
'lastImeTargetWindow':
this.item?.obj?.inputMethodManagerService?.lastImeTargetWindowName,
'inputShown': this.item?.obj?.inputMethodManagerService?.inputShown,
};
} else {
return null;
}
},
},
components: {
'tree-view': TreeView,
'rects': Rects,
'flat-card': FlatCard,
'sf-property-groups': SurfaceFlingerPropertyGroups,
'properties-table-view': PropertiesTableView,
'ime-additional-properties': ImeAdditionalProperties,
},
};
</script>
<style scoped>
.container {
display: flex;
flex-wrap: wrap;
}
.rects {
flex: none;
margin: 8px;
}
.hierarchy,
.properties {
flex: 1;
margin: 8px;
min-width: 400px;
min-height: 70rem;
max-height: 70rem;
}
.rects,
.hierarchy,
.properties {
padding: 5px;
}
.flat-card {
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
}
.hierarchy>.tree-view,
.properties>.tree-view {
margin: 16px;
}
.treeview {
overflow: auto;
white-space: pre-line;
flex: 1 0 0;
}
.no-properties {
display: flex;
flex: 1;
flex-direction: column;
align-self: center;
align-items: center;
justify-content: center;
padding: 50px 25px;
}
.no-properties .none-icon {
font-size: 35px;
margin-bottom: 10px;
}
.no-properties span {
font-weight: 100;
}
.filter {
width: auto;
}
.element-summary {
padding: 1rem;
border-bottom: thin solid rgba(0,0,0,.12);
}
.element-summary .key {
font-weight: 500;
}
.element-summary .value {
color: rgba(0, 0, 0, 0.75);
}
.hierarchy-content,
.properties-content {
display: flex;
flex-direction: column;
flex: 1;
}
.tree-view-wrapper {
display: flex;
flex-direction: column;
flex: 1;
}
.height-reduced {
height: 55%;
}
.ime-additional-properties {
padding-top: 10px;
display: flex;
flex-direction: column;
flex: 1;
height: 45%;
overflow: auto;
}
</style>

View File

@@ -1,292 +0,0 @@
<template>
<div>
<div v-if="source.type === 'vsyncEvent'" class="vsync">
<div class="vsync-dot" />
<md-tooltip md-direction="left">
VSync
</md-tooltip>
</div>
<div v-else
class="entry"
:class="{
inactive: source.timestamp > currentTimestamp,
selected: isSelected
}"
@click="onClick(source)"
>
<div class="time-column">
<a @click="e => setTimelineTime(e, source.timestamp)" class="time-link">
{{source.time}}
</a>
<div
class="new-badge"
:style="{visibility: source.new ? 'visible' : 'hidden'} "
>
New
</div>
</div>
<div class="type-column">{{transactionTypeOf(source)}}</div>
<div class="affected-surfaces-column">
<span
v-for="(surface, index) in sufacesAffectedBy(source)"
v-bind:key="surface.id"
>
<span
v-if="simplifyNames && surface.shortName &&
surface.shortName !== surface.name"
>{{surface.shortName}}>
</span>
<span v-else>
<!-- eslint-disable-next-line max-len -->
<span v-if="surface.name" class="surface-name">{{ surface.name }}</span>
<span class="surface-id">
<!-- eslint-disable-next-line max-len -->
<span v-if="surface.name">(</span>{{surface.id}}<span v-if="surface.name">)</span>
</span>
<!-- eslint-disable-next-line max-len -->
<span v-if="index + 1 < sufacesAffectedBy(source).length">,&nbsp;</span>
</span>
</span>
</div>
<div class="extra-info-column">
<span v-if="source.identifier">
<!-- eslint-disable-next-line max-len -->
Tx Id: <span class="light">{{ prettifyTransactionId(source.identifier) }}</span><br/>
</span>
<span v-if="source.origin">
PID: <span class="light">{{ source.origin.pid }}</span><br/>
UID: <span class="light">{{ source.origin.uid }}</span><br/>
</span>
</div>
</div>
</div>
</template>
<script>
import { shortenName } from './flickerlib/mixin'
export default {
name: 'transaction-entry-legacy',
props: {
index: {
type: Number,
},
source: {
type: Object,
default() {
return {};
},
},
onClick: {
type: Function,
},
selectedTransaction: {
type: Object,
},
transactionsTrace: {
type: Object,
},
prettifyTransactionId: {
type: Function,
},
simplifyNames: {
type: Boolean,
},
},
computed: {
currentTimestamp() {
return this.$store.state.currentTimestamp;
},
isSelected() {
return this.source === this.selectedTransaction;
},
hasOverrideChangeDueToMerge() {
const transaction = this.source;
if (!transaction.identifier) {
return;
}
// console.log('transaction', transaction.identifier);
// const history = this.transactionsTrace.transactionHistory;
// const allTransactionsMergedInto = history
// .allTransactionsMergedInto(transaction.identifier);
// console.log('All merges', allTransactionsMergedInto);
// console.log('Direct merges',
// history.allDirectMergesInto(transaction.identifier));
return true;
},
},
methods: {
setTimelineTime(e, timestamp) {
e.preventDefault();
e.stopPropagation();
this.$store.dispatch('updateTimelineTime', timestamp);
},
transactionTypeOf(transaction) {
if (transaction.type !== 'transaction') {
return transaction.type;
}
if (transaction.transactions.length === 0) {
return 'Empty Transaction';
}
const types = new Set();
transaction.transactions.forEach((t) => types.add(t.type));
return Array.from(types).join(', ');
},
sufacesAffectedBy(transaction) {
if (transaction.type !== 'transaction') {
return [
{
name: transaction.layerName,
shortName: shortenName(transaction.layerName),
id: transaction.obj.id
}];
}
const surfaceIds = new Set();
const affectedSurfaces = [];
for (const transaction of transaction.transactions) {
const id = transaction.obj.id;
if (!surfaceIds.has(id)) {
surfaceIds.add(id);
affectedSurfaces.push(
{
name: transaction.layerName,
shortName: shortenName(transaction.layerName),
id
});
}
}
return affectedSurfaces;
},
},
};
</script>
<style scoped>
.time-column {
display: inline-flex;
width: 13em;
}
.time-column .time-link {
width: 9em;
}
.type-column {
width: 12em;
}
.origin-column {
width: 9em;
}
.affected-surfaces-column {
word-wrap: break-word;
width: 30em;
}
.extra-info-column {
width: 20em;
}
.entry {
display: inline-flex;
cursor: pointer;
}
.entry > div {
padding: 6px 10px;
border-bottom: 1px solid #f1f1f1;
}
.entry.selected {
background-color: #365179;
color: white;
}
.entry.selected a {
color: white;
}
.entry:not(.selected):hover {
background: #f1f1f1;
}
a {
cursor: pointer;
}
.inactive {
color: gray;
}
.inactive a {
color: gray;
}
.new-badge {
display: inline-block;
background: rgb(84, 139, 247);
border-radius: 3px;
color: white;
padding: 0 5px;
margin-left: 5px;
font-size: 10px;
}
.affected-surfaces-column .surface-id {
color: #999999
}
.inactive .affected-surfaces-column .surface-id {
color: #b4b4b4
}
.light {
color: #999999
}
.inactive .light {
color: #b4b4b4
}
.vsync {
position: relative;
}
.vsync-dot:before {
content: "";
position: absolute;
left: 0;
top: -5px;
height: 10px;
width: 10px;
background-color: rgb(170, 65, 255);
border-radius: 50%;
display: inline-block;
}
.vsync-dot:after {
content: "";
position: absolute;
left: 0;
top: 0;
height: 1px;
width: 100%;
background-color: rgb(170, 65, 255);
display: inline-block;
}
</style>

View File

@@ -1,545 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<md-card-content class="container">
<flat-card class="changes card">
<md-content
md-tag="md-toolbar"
md-elevation="0"
class="card-toolbar md-transparent md-dense"
>
<h2 class="md-title" style="flex: 1">Transactions</h2>
</md-content>
<div class="filters">
<div class="input">
<md-field>
<label>Transaction Type</label>
<md-select v-model="selectedTransactionTypes" multiple>
<md-option
v-for="type in transactionTypes"
:value="type"
v-bind:key="type">
{{ type }}
</md-option>
</md-select>
</md-field>
</div>
<div class="input">
<div>
<md-field>
<label>Changed property</label>
<md-select v-model="selectedProperties" multiple>
<md-option
v-for="property in properties"
:value="property"
v-bind:key="property">
{{ property }}
</md-option>
</md-select>
</md-field>
</div>
</div>
<div class="input">
<md-field>
<label>Origin PID</label>
<md-select v-model="selectedPids" multiple>
<md-option v-for="pid in pids" :value="pid" v-bind:key="pid">
{{ pid }}
</md-option>
</md-select>
</md-field>
</div>
<div class="input">
<md-field>
<label>Origin UID</label>
<md-select v-model="selectedUids" multiple>
<md-option v-for="uid in uids" :value="uid" v-bind:key="uid">
{{ uid }}
</md-option>
</md-select>
</md-field>
</div>
<div class="input">
<md-chips
v-model="filters"
md-placeholder="Add surface id or name..."
>
<div class="md-helper-text">Press enter to add</div>
</md-chips>
</div>
<md-checkbox v-model="trace.simplifyNames">
Simplify names
</md-checkbox>
</div>
<virtual-list style="height: 600px; overflow-y: auto;"
:data-key="'timestamp'"
:data-sources="filteredData"
:data-component="transactionEntryComponent"
:extra-props="{
onClick: transactionSelected,
selectedTransaction,
transactionsTrace,
prettifyTransactionId,
simplifyNames: trace.simplifyNames,
}"
ref="loglist"
/>
</flat-card>
<flat-card class="changes card">
<md-content
md-tag="md-toolbar"
md-elevation="0"
class="card-toolbar md-transparent md-dense"
>
<h2 class="md-title" style="flex: 1">Changes</h2>
</md-content>
<div class="changes-content" v-if="selectedTree">
<div
v-if="selectedTransaction.type === 'transaction'"
class="transaction-events"
>
<div
v-for="(event, i) in transactionHistory(selectedTransaction)"
v-bind:key="`${selectedTransaction.identifier}-${i}`"
class="transaction-event"
>
<div v-if="event.type === 'apply'" class="applied-event">
applied
</div>
<div v-if="event.type === 'merge'" class="merged-event">
<!-- eslint-disable-next-line max-len -->
{{ prettifyTransactionId(event.mergedId) }}
</div>
</div>
</div>
<tree-view
:item="selectedTree"
:collapseChildren="true"
:useGlobalCollapsedState="true"
/>
</div>
<div class="no-properties" v-else>
<i class="material-icons none-icon">
filter_none
</i>
<span>No transaction selected.</span>
</div>
</flat-card>
</md-card-content>
</template>
<script>
import TreeView from './TreeView.vue';
import VirtualList from '../libs/virtualList/VirtualList';
import TransactionEntryLegacy from './TransactionEntryLegacy.vue';
import FlatCard from './components/FlatCard.vue';
import {ObjectTransformer} from './transform.js';
import {expandTransactionId} from '@/traces/TransactionsLegacy.ts';
/**
* @deprecated This trace has been replaced by the new transactions trace
*/
export default {
name: 'transactionsviewlegacy',
props: ['trace'],
data() {
const transactionTypes = new Set();
const properties = new Set();
const pids = new Set();
const uids = new Set();
const transactionsTrace = this.trace;
for (const entry of transactionsTrace.data) {
if (entry.type == 'transaction') {
for (const transaction of entry.transactions) {
transactionTypes.add(transaction.type);
Object.keys(transaction.obj).forEach((item) => properties.add(item));
}
} else {
transactionTypes.add(entry.type);
Object.keys(entry.obj).forEach((item) => properties.add(item));
}
if (entry.origin) {
pids.add(entry.origin.pid);
uids.add(entry.origin.uid);
}
}
// Remove vsync from being transaction types that can be filtered
// We want to always show vsyncs
transactionTypes.delete('vsyncEvent');
return {
transactionTypes: Array.from(transactionTypes),
properties: Array.from(properties),
pids: Array.from(pids),
uids: Array.from(uids),
selectedTransactionTypes: [],
selectedPids: [],
selectedUids: [],
searchInput: '',
selectedTree: null,
filters: [],
selectedProperties: [],
selectedTransaction: null,
transactionEntryComponent: TransactionEntryLegacy,
transactionsTrace,
expandTransactionId,
};
},
computed: {
data() {
// Record analytics event
this.recordOpenTraceEvent("TransactionsTrace");
return this.transactionsTrace.data;
},
filteredData() {
let filteredData = this.data;
if (this.selectedTransactionTypes.length > 0) {
filteredData = filteredData.filter(
this.filterTransactions((transaction) =>
transaction.type === 'vsyncEvent' ||
this.selectedTransactionTypes.includes(transaction.type)));
}
if (this.selectedPids.length > 0) {
filteredData = filteredData.filter((entry) =>
this.selectedPids.includes(entry.origin?.pid));
}
if (this.selectedUids.length > 0) {
filteredData = filteredData.filter((entry) =>
this.selectedUids.includes(entry.origin?.uid));
}
if (this.filters.length > 0) {
filteredData = filteredData.filter(
this.filterTransactions((transaction) => {
for (const filter of this.filters) {
if (isNaN(filter)) {
// If filter isn't a number then check if the transaction's
// target surface's name matches the filter — if so keep it.
const regexFilter = new RegExp(filter, "i");
if (regexFilter.test(transaction.layerName)) {
return true;
}
}
if (filter == transaction.obj.id) {
// If filteter is a number then check if the filter matches
// the transaction's target surface id — if so keep it.
return true;
}
}
// Exclude transaction if it fails to match filter.
return false;
}),
);
}
if (this.selectedProperties.length > 0) {
const regexFilter = new RegExp(this.selectedProperties.join("|"), "i");
filteredData = filteredData.filter(
this.filterTransactions((transaction) => {
for (const key in transaction.obj) {
if (this.isMeaningfulChange(transaction.obj, key) && regexFilter.test(key)) {
return true;
}
}
return false;
}),
);
}
// We quish vsyncs because otherwise the lazy list will not load enough
// elements if there are many vsyncs in a row since vsyncs take up no
// space.
return this.squishVSyncs(filteredData);
},
},
methods: {
removeNullFields(changeObject) {
for (const key in changeObject) {
if (changeObject[key] === null) {
delete changeObject[key];
}
}
return changeObject;
},
transactionSelected(transaction) {
this.selectedTransaction = transaction;
const META_DATA_KEY = 'metadata';
let obj;
let name;
if (transaction.type == 'transaction') {
name = 'changes';
obj = {};
const [surfaceChanges, displayChanges] =
this.aggregateTransactions(transaction.transactions);
// Prepare the surface and display changes to be passed through
// the ObjectTransformer — in particular, remove redundant properties
// and add metadata that can be accessed post transformation
const perpareForTreeViewTransform = (change) => {
this.removeNullFields(change);
change[META_DATA_KEY] = {
layerName: change.layerName,
};
// remove redundant properties
delete change.layerName;
delete change.id;
};
for (const changeId in surfaceChanges) {
if (surfaceChanges.hasOwnProperty(changeId)) {
perpareForTreeViewTransform(surfaceChanges[changeId]);
}
}
for (const changeId in displayChanges) {
if (displayChanges.hasOwnProperty(changeId)) {
perpareForTreeViewTransform(displayChanges[changeId]);
}
}
if (Object.keys(surfaceChanges).length > 0) {
obj.surfaceChanges = surfaceChanges;
}
if (Object.keys(displayChanges).length > 0) {
obj.displayChanges = displayChanges;
}
} else {
obj = this.removeNullFields(transaction.obj);
name = transaction.type;
}
// Transform the raw JS object to be TreeView compatible
const transactionUniqueId = transaction.timestamp;
let tree = new ObjectTransformer(
obj,
name,
transactionUniqueId,
).setOptions({
formatter: () => {},
}).transform({
keepOriginal: true,
metadataKey: META_DATA_KEY,
freeze: false,
});
// Add the layer name as the kind of the object to be shown in the
// TreeView
const addLayerNameAsKind = (tree) => {
for (const layerChanges of tree.children) {
layerChanges.kind = layerChanges.metadata.layerName;
}
};
if (transaction.type == 'transaction') {
for (const child of tree.children) {
// child = surfaceChanges or displayChanges tree node
addLayerNameAsKind(child);
}
}
// If there are only surfaceChanges or only displayChanges and not both
// remove the extra top layer node which is meant to hold both types of
// changes when both are present
if (tree.name == 'changes' && tree.children.length === 1) {
tree = tree.children[0];
}
this.selectedTree = tree;
},
filterTransactions(condition) {
return (entry) => {
if (entry.type == 'transaction') {
for (const transaction of entry.transactions) {
if (condition(transaction)) {
return true;
}
}
return false;
} else {
return condition(entry);
}
};
},
isMeaningfulChange(object, key) {
// TODO (b/159799733): Handle cases of non null objects but meaningless
// change
return object[key] !== null && object.hasOwnProperty(key);
},
mergeChanges(a, b) {
const res = {};
for (const key in a) {
if (this.isMeaningfulChange(a, key)) {
res[key] = a[key];
}
}
for (const key in b) {
if (this.isMeaningfulChange(b, key)) {
if (res.hasOwnProperty(key) && key != 'id') {
throw new Error(`Merge failed key '${key}' already present`);
}
res[key] = b[key];
}
}
return res;
},
aggregateTransactions(transactions) {
const surfaceChanges = {};
const displayChanges = {};
for (const transaction of transactions) {
const obj = transaction.obj;
// Create a new base object to merge all changes into
const newBaseObj = () => {
return {
layerName: transaction.layerName,
};
};
switch (transaction.type) {
case 'surfaceChange':
surfaceChanges[obj.id] =
this.mergeChanges(surfaceChanges[obj.id] ?? newBaseObj(), obj);
break;
case 'displayChange':
displayChanges[obj.id] =
this.mergeChanges(displayChanges[obj.id] ?? newBaseObj(), obj);
break;
default:
throw new Error(`Unhandled transaction type ${transaction.type}`);
}
}
return [surfaceChanges, displayChanges];
},
transactionHistory(selectedTransaction) {
const transactionId = selectedTransaction.identifier;
const history = this.transactionsTrace.transactionHistory
.generateHistoryTreesOf(transactionId);
return history;
},
prettifyTransactionId(transactionId) {
const expandedId = expandTransactionId(transactionId);
return `${expandedId.pid}.${expandedId.id}`;
},
squishVSyncs(data) {
return data.filter((event, i) => {
return !(event.type === 'vsyncEvent' &&
data[i + 1]?.type === 'vsyncEvent');
});
},
},
components: {
'virtual-list': VirtualList,
'tree-view': TreeView,
'flat-card': FlatCard,
},
};
</script>
<style scoped>
.container {
display: flex;
flex-wrap: wrap;
}
.transaction-table,
.changes {
flex: 1 1 0;
width: 0;
margin: 8px;
}
.scrollBody {
width: 100%;
height: 100%;
overflow: scroll;
}
.filters {
margin-bottom: 15px;
width: 100%;
padding: 15px 5px;
display: flex;
flex-wrap: wrap;
}
.filters .input {
max-width: 300px;
margin: 0 10px;
flex-grow: 1;
}
.changes-content {
padding: 18px;
height: 550px;
overflow: auto;
}
.no-properties {
display: flex;
flex-direction: column;
align-self: center;
align-items: center;
justify-content: center;
height: calc(100% - 50px);
padding: 50px 25px;
}
.no-properties .none-icon {
font-size: 35px;
margin-bottom: 10px;
}
.no-properties span {
font-weight: 100;
}
.transaction-event {
display: inline-flex;
}
</style>

View File

@@ -1,59 +0,0 @@
<!-- 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.
-->
<template>
<div class="matrix" v-if="transform">
<md-tooltip>{{ transform.getTypeAsString() }}</md-tooltip>
<div class="cell">{{ formatFloat(transform.matrix.dsdx) }}</div>
<div class="cell">{{ formatFloat(transform.matrix.dsdy) }}</div>
<div class="cell">
{{ formatFloat(transform.matrix.tx) }}<md-tooltip>Translate x</md-tooltip>
</div>
<div class="cell">{{ formatFloat(transform.matrix.dtdx) }}</div>
<div class="cell">{{ formatFloat(transform.matrix.dtdy) }}</div>
<div class="cell">
{{ formatFloat(transform.matrix.ty) }}<md-tooltip>Translate y</md-tooltip>
</div>
<div class="cell">0</div>
<div class="cell">0</div>
<div class="cell">1</div>
</div>
</template>
<script>
export default {
name: "TransformMatrix",
props: ["transform"],
methods: {
formatFloat(num) {
return Math.round(num * 100) / 100;
},
},
};
</script>
<style scoped>
.matrix {
display: grid;
grid-gap: 1px;
grid-template-columns: repeat(3, 1fr);
}
.cell {
padding-left: 10px;
background-color: rgba(158, 192, 200, 0.281);
}
</style>

View File

@@ -1,739 +0,0 @@
<!-- Copyright (C) 2017 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.
-->
<template>
<div class="tree-view" v-if="item">
<div class="node"
:class="[{
leaf: isLeaf,
selected: isSelected,
'child-selected': immediateChildSelected,
clickable: isClickable,
hover: nodeHover,
'child-hover': childHover,
}, diffClass]"
:style="nodeOffsetStyle"
@click="clicked"
@contextmenu.prevent="openContextMenu"
ref="node"
>
<button
class="toggle-tree-btn"
@click="toggleTree"
v-if="!isLeaf && !flattened"
v-on:click.stop
>
<i aria-hidden="true" class="md-icon md-theme-default material-icons">
{{isCollapsed ? "chevron_right" : "expand_more"}}
</i>
</button>
<div class="leaf-node-icon-wrapper" v-else>
<i class="leaf-node-icon"/>
</div>
<div class="description">
<div v-if="elementView">
<component
:is="elementView"
:item="item"
:simplify-names="simplifyNames"
/>
</div>
<div v-else>
<DefaultTreeElement
:item="item"
:simplify-names="simplifyNames"
:errors="errors"
:transitions="transitions"
/>
</div>
</div>
<div v-show="isCollapsed">
<button
class="expand-tree-btn"
:class="[{
'child-selected': isCollapsed && childIsSelected
}, collapseDiffClass]"
v-if="children"
@click="expandTree"
v-on:click.stop
>
<i
aria-hidden="true"
class="md-icon md-theme-default material-icons"
>
more_horiz
</i>
</button>
</div>
</div>
<node-context-menu
ref="nodeContextMenu"
v-on:collapseAllOtherNodes="collapseAllOtherNodes"
/>
<div class="children" v-if="children" v-show="!isCollapsed" :style="childrenIndentation()">
<tree-view
v-for="(c,i) in children"
:item="c"
@item-selected="childItemSelected"
:selected="selected"
:key="i"
:filter="childFilter(c)"
:flattened="flattened"
:onlyVisible="onlyVisible"
:simplify-names="simplifyNames"
:flickerTraceView="flickerTraceView"
:presentTags="currentTags"
:presentErrors="currentErrors"
:force-flattened="applyingFlattened"
v-show="filterMatches(c)"
:items-clickable="itemsClickable"
:initial-depth="depth + 1"
:collapse="collapseChildren"
:collapseChildren="collapseChildren"
:useGlobalCollapsedState="useGlobalCollapsedState"
v-on:hoverStart="childHover = true"
v-on:hoverEnd="childHover = false"
v-on:selected="immediateChildSelected = true"
v-on:unselected="immediateChildSelected = false"
:elementView="elementView"
v-on:collapseSibling="collapseSibling"
v-on:collapseAllOtherNodes="collapseAllOtherNodes"
v-on:closeAllContextMenus="closeAllContextMenus"
ref="children"
/>
</div>
</div>
</template>
<script>
import DefaultTreeElement from './DefaultTreeElement.vue';
import NodeContextMenu from './NodeContextMenu.vue';
import {DiffType} from './utils/diff.js';
import {isPropertyMatch} from './utils/utils.js';
/* in px, must be kept in sync with css, maybe find a better solution... */
const levelOffset = 24;
export default {
name: 'tree-view',
props: [
'item',
'selected',
'filter',
'simplify-names',
'flattened',
'force-flattened',
'items-clickable',
'initial-depth',
'collapse',
'collapseChildren',
// Allows collapse state to be tracked by Vuex so that collapse state of
// items with same stableId can remain consisten accross time and easily
// toggled from anywhere in the app.
// Should be true if you are using the same TreeView to display multiple
// trees throughout the component's lifetime to make sure same nodes are
// toggled when switching back and forth between trees.
// If true, requires all nodes in tree to have a stableId.
'useGlobalCollapsedState',
// Custom view to use to render the elements in the tree view
'elementView',
'onlyVisible',
'flickerTraceView',
'presentTags',
'presentErrors',
],
data() {
const isCollapsedByDefault = this.collapse ?? false;
return {
isChildSelected: false,
immediateChildSelected: false,
clickTimeout: null,
isCollapsedByDefault,
localCollapsedState: isCollapsedByDefault,
collapseDiffClass: null,
nodeHover: false,
childHover: false,
diffSymbol: {
[DiffType.NONE]: '',
[DiffType.ADDED]: '+',
[DiffType.DELETED]: '-',
[DiffType.MODIFIED]: '.',
[DiffType.MOVED]: '.',
},
currentTags: [],
currentErrors: [],
transitions: [],
errors: [],
};
},
watch: {
stableId() {
// Update anything that is required to change when item changes.
this.updateCollapsedDiffClass();
},
hasDiff(hasDiff) {
if (!hasDiff) {
this.collapseDiffClass = null;
} else {
this.updateCollapsedDiffClass();
}
},
currentTimestamp() {
// Update anything that is required to change when time changes.
this.currentTags = this.getCurrentItems(this.presentTags);
this.currentErrors = this.getCurrentItems(this.presentErrors);
this.transitions = this.getCurrentTransitions();
this.errors = this.getCurrentErrorTags();
this.updateCollapsedDiffClass();
},
isSelected(isSelected) {
if (isSelected) {
this.$emit('selected');
} else {
this.$emit('unselected');
}
},
},
methods: {
setCollapseValue(isCollapsed) {
if (this.useGlobalCollapsedState) {
this.$store.commit('setCollapsedState', {
item: this.item,
isCollapsed,
});
} else {
this.localCollapsedState = isCollapsed;
}
},
toggleTree() {
this.setCollapseValue(!this.isCollapsed);
if (!this.isCollapsed) {
this.recordExpandedPropertyEvent(this.item.name);
}
},
expandTree() {
this.setCollapseValue(false);
},
selectNext(found, inCollapsedTree) {
// Check if this is the next visible item
if (found && this.filterMatches(this.item) && !inCollapsedTree) {
this.select();
return false;
}
// Set traversal state variables
if (this.isSelected) {
found = true;
}
if (this.isCollapsed) {
inCollapsedTree = true;
}
// Travers children trees recursively in reverse to find currently
// selected item and select the next visible one
if (this.$refs.children) {
for (const c of this.$refs.children) {
found = c.selectNext(found, inCollapsedTree);
}
}
return found;
},
selectPrev(found, inCollapsedTree) {
// Set inCollapseTree flag to make sure elements in collapsed trees are
// not selected.
const isRootCollapse = !inCollapsedTree && this.isCollapsed;
if (isRootCollapse) {
inCollapsedTree = true;
}
// Travers children trees recursively in reverse to find currently
// selected item and select the previous visible one
if (this.$refs.children) {
for (const c of [...this.$refs.children].reverse()) {
found = c.selectPrev(found, inCollapsedTree);
}
}
// Unset inCollapseTree flag as we are no longer in a collapsed tree.
if (isRootCollapse) {
inCollapsedTree = false;
}
// Check if this is the previous visible item
if (found && this.filterMatches(this.item) && !inCollapsedTree) {
this.select();
return false;
}
// Set found flag so that the next visited visible item can be selected.
if (this.isSelected) {
found = true;
}
return found;
},
childItemSelected(item) {
this.isChildSelected = true;
this.$emit('item-selected', item);
},
select() {
this.$emit('item-selected', this.item);
},
clicked(e) {
if (window.getSelection().type === 'range') {
// Ignore click if is selection
return;
}
if (!this.isLeaf && e.detail % 2 === 0) {
// Double click collapsable node
this.toggleTree();
} else {
this.select();
}
},
filterMatches(c) {
// If a filter is set, consider the item matches if the current item or
// any of its children matches.
if (this.filter) {
const thisMatches = this.filter(c);
const childMatches = (child) => this.filterMatches(child);
return thisMatches || (!this.applyingFlattened &&
c.children && c.children.some(childMatches));
}
return true;
},
childFilter(c) {
if (this.filter) {
if (this.filter(c)) {
// Filter matched c, don't apply further filtering on c's children.
return undefined;
}
}
return this.filter;
},
isCurrentSelected() {
return this.selected === this.item;
},
updateCollapsedDiffClass() {
// NOTE: Could be memoized in $store map like collapsed state if
// performance ever becomes a problem.
if (this.item) {
this.collapseDiffClass = this.computeCollapseDiffClass();
}
},
getAllDiffTypesOfChildren(item) {
if (!item.children) {
return new Set();
}
const classes = new Set();
for (const child of item.children) {
if (child.diff) {
classes.add(child.diff.type);
}
for (const diffClass of this.getAllDiffTypesOfChildren(child)) {
classes.add(diffClass);
}
}
return classes;
},
computeCollapseDiffClass() {
if (!this.isCollapsed) {
return '';
}
const childrenDiffClasses = this.getAllDiffTypesOfChildren(this.item);
childrenDiffClasses.delete(DiffType.NONE);
childrenDiffClasses.delete(undefined);
if (childrenDiffClasses.size === 0) {
return '';
}
if (childrenDiffClasses.size === 1) {
const diff = childrenDiffClasses.values().next().value;
return diff;
}
return DiffType.MODIFIED;
},
collapseAllOtherNodes() {
this.$emit('collapseAllOtherNodes');
this.$emit('collapseSibling', this.item);
},
collapseSibling(item) {
if (!this.$refs.children) {
return;
}
for (const child of this.$refs.children) {
if (child.item === item) {
continue;
}
child.collapseAll();
}
},
collapseAll() {
this.setCollapseValue(true);
if (!this.$refs.children) {
return;
}
for (const child of this.$refs.children) {
child.collapseAll();
}
},
openContextMenu(e) {
this.closeAllContextMenus();
// vue-context takes in the event and uses clientX and clientY to
// determine the position of the context meny.
// This doesn't satisfy our use case so we specify our own positions for
// this.
this.$refs.nodeContextMenu.open({
clientX: e.x,
clientY: e.y,
});
},
closeAllContextMenus(requestOrigin) {
this.$refs.nodeContextMenu.close();
this.$emit('closeAllContextMenus', this.item);
this.closeAllChildrenContextMenus(requestOrigin);
},
closeAllChildrenContextMenus(requestOrigin) {
if (!this.$refs.children) {
return;
}
for (const child of this.$refs.children) {
if (child.item === requestOrigin) {
continue;
}
child.$refs.nodeContextMenu.close();
child.closeAllChildrenContextMenus();
}
},
childrenIndentation() {
if (this.flattened || this.forceFlattened) {
return {
marginLeft: '0px',
paddingLeft: '0px',
marginTop: '0px',
};
} else {
// Aligns border with collapse arrows
return {
marginLeft: '12px',
paddingLeft: '11px',
borderLeft: '1px solid rgb(238, 238, 238)',
marginTop: '0px',
};
}
},
/** Performs check for id match between entry and present tags/errors
* exits once match has been found
*/
matchItems(flickerItems) {
let match = false;
flickerItems.every((flickerItem) => {
if (isPropertyMatch(flickerItem, this.item)) {
match = true;
return false;
}
});
return match;
},
/** Returns check for id match between entry and present tags/errors */
isEntryTagMatch() {
return this.matchItems(this.currentTags) || this.matchItems(this.currentErrors);
},
getCurrentItems(items) {
if (!items) return [];
else return items.filter((item) => item.timestamp===this.currentTimestamp);
},
getCurrentTransitions() {
const transitions = [];
const ids = [];
this.currentTags.forEach((tag) => {
if (!ids.includes(tag.id) && isPropertyMatch(tag, this.item)) {
transitions.push(tag.transition);
ids.push(tag.id);
}
});
return transitions;
},
getCurrentErrorTags() {
return this.currentErrors.filter((error) => isPropertyMatch(error, this.item));
},
},
computed: {
hasDiff() {
return this.item?.diff !== undefined;
},
stableId() {
return this.item?.stableId;
},
currentTimestamp() {
return this.$store.state.currentTimestamp;
},
isCollapsed() {
if (!this.item.children || this.item.children?.length === 0) {
return false;
}
if (this.useGlobalCollapsedState) {
return this.$store.getters.collapsedStateStoreFor(this.item) ??
this.isCollapsedByDefault;
}
return this.localCollapsedState;
},
isSelected() {
return this.selected === this.item;
},
childIsSelected() {
if (this.$refs.children) {
for (const c of this.$refs.children) {
if (c.isSelected || c.childIsSelected) {
return true;
}
}
}
return false;
},
diffClass() {
return this.item.diff ? this.item.diff.type : '';
},
applyingFlattened() {
return (this.flattened && this.item.flattened) || this.forceFlattened;
},
children() {
return this.applyingFlattened ? this.item.flattened : this.item.children;
},
isLeaf() {
return !this.children || this.children.length === 0;
},
isClickable() {
return !this.isLeaf || this.itemsClickable;
},
depth() {
return this.initialDepth || 0;
},
nodeOffsetStyle() {
const offset = levelOffset * (this.depth + this.isLeaf) + 'px';
let display = '';
if (!this.item.timestamp &&
this.flattened &&
(this.onlyVisible && !this.item.isVisible ||
this.flickerTraceView && !this.isEntryTagMatch())) {
display = 'none';
}
return {
marginLeft: '-' + offset,
paddingLeft: offset,
display: display,
};
},
},
mounted() {
// Prevent highlighting on multiclick of node element
this.nodeMouseDownEventListner = (e) => {
if (e.detail > 1) {
e.preventDefault();
return false;
}
return true;
};
this.$refs.node?.addEventListener('mousedown',
this.nodeMouseDownEventListner);
this.updateCollapsedDiffClass();
this.nodeMouseEnterEventListener = (e) => {
this.nodeHover = true;
this.$emit('hoverStart');
};
this.$refs.node?.addEventListener('mouseenter',
this.nodeMouseEnterEventListener);
this.nodeMouseLeaveEventListener = (e) => {
this.nodeHover = false;
this.$emit('hoverEnd');
};
this.$refs.node?.addEventListener('mouseleave',
this.nodeMouseLeaveEventListener);
},
beforeDestroy() {
this.$refs.node?.removeEventListener('mousedown',
this.nodeMouseDownEventListner);
this.$refs.node?.removeEventListener('mouseenter',
this.nodeMouseEnterEventListener);
this.$refs.node?.removeEventListener('mouseleave',
this.nodeMouseLeaveEventListener);
},
components: {
DefaultTreeElement,
NodeContextMenu,
},
};
</script>
<style>
.data-card > .tree-view {
border: none;
}
.tree-view {
display: block;
}
.tree-view .node {
display: flex;
padding: 2px;
align-items: flex-start;
}
.tree-view .node.clickable {
cursor: pointer;
}
.tree-view .node:hover:not(.selected) {
background: #f1f1f1;
}
.tree-view .node:not(.selected).added,
.tree-view .node:not(.selected).addedMove,
.tree-view .expand-tree-btn.added,
.tree-view .expand-tree-btn.addedMove {
background: #03ff35;
}
.tree-view .node:not(.selected).deleted,
.tree-view .node:not(.selected).deletedMove,
.tree-view .expand-tree-btn.deleted,
.tree-view .expand-tree-btn.deletedMove {
background: #ff6b6b;
}
.tree-view .node:not(.selected).modified,
.tree-view .expand-tree-btn.modified {
background: cyan;
}
.tree-view .node.addedMove:after,
.tree-view .node.deletedMove:after {
content: 'moved';
margin: 0 5px;
background: #448aff;
border-radius: 5px;
padding: 3px;
color: white;
}
.tree-view .node.child-selected + .children {
border-left: 1px solid #b4b4b4;
}
.tree-view .node.selected + .children {
border-left: 1px solid rgb(200, 200, 200);
}
.tree-view .node.child-hover + .children {
border-left: 1px solid #b4b4b4;
}
.tree-view .node.hover + .children {
border-left: 1px solid rgb(200, 200, 200);
}
.kind {
color: #333;
font-weight: bold;
}
.selected {
background-color: #365179;
color: white;
}
.childSelected {
border-left: 1px solid rgb(233, 22, 22)
}
.selected .kind {
color: #e9e9e9;
}
.toggle-tree-btn, .expand-tree-btn {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
}
.expand-tree-btn {
margin-left: 5px;
}
.expand-tree-btn.child-selected {
color: #3f51b5;
}
.description {
display: flex;
flex: 1 1 auto;
word-break: break-all;
}
.description > div {
display: flex;
flex: 1 1 auto;
}
.leaf-node-icon-wrapper {
width: 24px;
height: 24px;
display: inline-flex;
align-content: center;
align-items: center;
justify-content: center;
}
.leaf-node-icon {
content: "";
display: inline-block;
height: 5px;
width: 5px;
margin-top: -2px;
border-radius: 50%;
background-color: #9b9b9b;
}
</style>

View File

@@ -1,87 +0,0 @@
<!-- Copyright (C) 2019 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.
-->
<template>
<video
class="md-elevation-2 screen"
:src="file.data"
:style="style"
ref="video"
/>
</template>
<script>
const EPSILON = 0.00001;
function uint8ToString(array) {
const chunk = 0x8000;
const out = [];
for (let i = 0; i < array.length; i += chunk) {
out.push(String.fromCharCode.apply(null, array.subarray(i, i + chunk)));
}
return out.join('');
}
export default {
name: 'videoview',
props: ['file', 'height'],
data() {
// Record analytics event
this.recordOpenTraceEvent("Video");
return {};
},
computed: {
selectedIndex() {
return this.file.selectedIndex;
},
style() {
if (typeof this.height == 'number') {
return `height: ${this.height}px`;
} else {
return `height: ${this.height}`;
}
},
},
methods: {
arrowUp() {
return true;
},
arrowDown() {
return true;
},
selectFrameAtTime(timestamp) {
const time = (timestamp - this.file.timeline[0]) / 1000000000 + EPSILON;
this.$refs.video.currentTime = time;
},
selectFrame(idx) {
this.selectFrameAtTime(this.file.timeline[idx]);
},
jumpToSelectedIndex() {
this.selectFrame(this.file.selectedIndex);
},
},
watch: {
selectedIndex() {
this.selectFrame(this.file.selectedIndex);
},
},
mounted() {
this.$el.addEventListener('canplay', (e) => {
this.$emit('loaded');
});
},
};
</script>
<style>
</style>

View File

@@ -1,48 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<TraceView
:store="store"
:file="file"
:summarizer="summarizer"
:presentTags="presentTags"
:presentErrors="presentErrors"
:propertyGroups="false"
/>
</template>
<script>
import TraceView from "@/TraceView.vue"
export default {
name: "WindowManagerTraceView",
props: ["store", "file", "presentTags", "presentErrors"],
components: {
TraceView
},
methods: {
summarizer(item) {
const summary = [];
if (item.isIncompleteReason) {
summary.push({key: 'Incomplete state reason', value: item.isIncompleteReason});
}
return summary;
},
}
}
</script>

View File

@@ -1,38 +0,0 @@
<template>
<div class="flat-card">
<slot />
</div>
</template>
<script>
export default {
name: 'FlatCard',
}
</script>
<style scoped>
.flat-card {
word-break: normal;
tab-size: 4;
font-size: 14px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: rgba(0,0,0,0);
font-family: Roboto, sans-serif;
line-height: 1.5;
background-repeat: no-repeat;
box-sizing: inherit;
padding: 0;
margin: 0;
display: block;
max-width: 100%;
outline: none;
text-decoration: none;
transition-property: box-shadow,opacity;
overflow-wrap: break-word;
position: relative;
white-space: normal;
border: thin solid rgba(0,0,0,.12);
background-color: #fff;
color: rgba(0,0,0,.87);
border-radius: 4px;
}
</style>

View File

@@ -1,164 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<md-menu-item
:class="optionClasses"
:disabled="isDisabled" @click="setSelection"
>
<md-checkbox
class="md-primary"
v-model="isChecked"
v-if="MdSelect.multiple"
:disabled="isDisabled"
/>
<div class="item">
<i class="material-icons" v-if="icon">
{{ icon }}
</i>
<span class="value">
{{ displayValue || value }}
</span>
<span class="material-icons help-icon">
help_outline
<md-tooltip md-direction="right">{{ desc }}</md-tooltip>
</span>
</div>
</md-menu-item>
</template>
<script>
export default {
name: 'MdIconOption',
props: {
// Serves as key for option (should be unique within an MdSelect)
// Also serves as the backup to displayValue if null
value: [String, Number, Boolean],
// Value shown to describe an option in dropdown selection
displayValue: [String, Number, Boolean],
// If present, this is shown to represent item when dropdown is collapsed
shortValue: [String, Number, Boolean],
icon: String,
desc: String,
disabled: Boolean,
},
inject: {
MdSelect: {},
MdOptgroup: {
default: {},
},
},
data: () => ({
isSelected: false,
isChecked: false,
}),
computed: {
selectValue() {
return this.MdSelect.modelValue;
},
isMultiple() {
return this.MdSelect.multiple;
},
isDisabled() {
return this.MdOptgroup.disabled || this.disabled;
},
key() {
return this.value;
},
inputLabel() {
return this.MdSelect.label;
},
optionClasses() {
return {
'md-selected': this.isSelected || this.isChecked,
};
},
},
watch: {
selectValue() {
this.setIsSelected();
},
isChecked(val) {
if (val === this.isSelected) {
return;
}
this.setSelection();
},
isSelected(val) {
this.isChecked = val;
},
},
methods: {
getTextContent() {
return this.shortValue || this.displayValue || this.value;
},
setIsSelected() {
if (!this.isMultiple) {
this.isSelected = this.selectValue === this.value;
return;
}
if (this.selectValue === undefined) {
this.isSelected = false;
return;
}
this.isSelected = this.selectValue.includes(this.value);
},
setSingleSelection() {
this.MdSelect.setValue(this.value);
},
setMultipleSelection() {
this.MdSelect.setMultipleValue(this.value);
},
setSelection() {
if (!this.isDisabled) {
if (this.isMultiple) {
this.setMultipleSelection();
} else {
this.setSingleSelection();
}
}
},
setItem() {
this.$set(this.MdSelect.items, this.key, this.getTextContent());
},
},
updated() {
this.setItem();
},
created() {
this.setItem();
this.setIsSelected();
},
};
</script>
<style scoped>
.item {
display: inline-flex;
align-items: center;
width: 100%;
flex: 1;
}
.item .value {
flex-grow: 1;
margin: 0 10px;
}
.item .help-icon {
font-size: 15px;
}
</style>

View File

@@ -1,33 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<div class="arrow"/>
</template>
<script>
export default {
name: 'arrow',
};
</script>
<style scoped>
.arrow {
display: inline-block;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 10px solid;
}
</style>

View File

@@ -1,113 +0,0 @@
<!-- Copyright (C) 2020 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.
-->
<template>
<div class="transition-container" :style="transitionStyle" @click="handleTransitionClick()">
<md-tooltip md-direction="left"> {{tooltip}} </md-tooltip>
<arrow class="arrow-start" :style="transitionComponentColor"/>
<div class="connector" :style="transitionComponentColor"/>
<arrow class="arrow-end" :style="transitionComponentColor"/>
</div>
</template>
<script>
import Arrow from './Arrow.vue';
import LocalStore from '../../localstore.js';
var transitionCount = false;
export default {
name: 'transition-container',
components: {
'arrow': Arrow,
},
props: {
'width': {
type: Number,
},
'startPos': {
type: Number,
},
'startTime': {
type: Number,
},
'endTime': {
type: Number,
},
'color': {
type: String,
},
'overlap': {
type: Number,
},
'tooltip': {
type: String,
},
'store': {
type: LocalStore,
},
},
methods: {
handleTransitionClick() {
if (transitionCount) {
this.$store.dispatch('updateTimelineTime', this.startTime);
transitionCount = false;
} else {
this.$store.dispatch('updateTimelineTime', this.endTime);
transitionCount = true;
}
},
},
computed: {
transitionStyle() {
return {
width: this.width + '%',
left: this.startPos + '%',
bottom: this.overlap * 100 + '%',
}
},
transitionComponentColor() {
return {
borderTopColor: this.color,
}
},
},
};
</script>
<style scoped>
.transition-container {
position: absolute;
height: 15px;
display: inline-flex;
}
.arrow-start {
position: absolute;
left: 0%;
}
.arrow-end {
position: absolute;
right: 0%;
}
.connector {
position: absolute;
display: inline-block;
width: auto;
height: 9px;
left: 5px;
right: 5px;
border-top: 1px solid;
}
</style>

View File

@@ -1,35 +0,0 @@
{
"invalidProperties": [
"length",
"prototype",
"ref",
"diff",
"rects",
"chips",
"parent",
"timestamp",
"shortName",
"kind",
"resolvedChildren",
"visibilityReason",
"absoluteZ",
"children",
"stableId"
],
"intDefColumn": {
"WindowLayoutParams.type": "android.view.WindowManager.LayoutParams.WindowType",
"WindowLayoutParams.flags": "android.view.WindowManager.LayoutParams.Flags",
"WindowLayoutParams.privateFlags": "android.view.WindowManager.LayoutParams.PrivateFlags",
"WindowLayoutParams.gravity": "android.view.Gravity.GravityFlags",
"WindowLayoutParams.softInputMode": "android.view.WindowManager.LayoutParams.WindowType",
"WindowLayoutParams.systemUiVisibilityFlags": "android.view.WindowManager.LayoutParams.SystemUiVisibilityFlags",
"WindowLayoutParams.subtreeSystemUiVisibilityFlags": "android.view.WindowManager.LayoutParams.SystemUiVisibilityFlags",
"WindowLayoutParams.behavior": "android.view.WindowInsetsController.Behavior",
"WindowLayoutParams.fitInsetsSides": "android.view.WindowInsets.Side.InsetsSide",
"Configuration.windowingMode": "android.app.WindowConfiguration.WindowingMode",
"WindowConfiguration.windowingMode": "android.app.WindowConfiguration.WindowingMode",
"Configuration.orientation": "android.content.pm.ActivityInfo.ScreenOrientation",
"WindowConfiguration.orientation": "android.content.pm.ActivityInfo.ScreenOrientation",
"WindowState.orientation": "android.content.pm.ActivityInfo.ScreenOrientation"
}
}

View File

@@ -1,734 +0,0 @@
/*
* Copyright 2017, 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.
*/
/* eslint-disable camelcase */
/* eslint-disable max-len */
import jsonProtoDefsAccessibility from 'frameworks/base/core/proto/android/server/accessibilitytrace.proto';
import jsonProtoDefsWm from 'frameworks/base/core/proto/android/server/windowmanagertrace.proto';
import jsonProtoDefsProtoLog from 'frameworks/base/core/proto/android/internal/protolog.proto';
import jsonProtoDefsSf from 'frameworks/native/services/surfaceflinger/layerproto/layerstrace.proto';
import jsonProtoDefsTransaction from 'frameworks/native/services/surfaceflinger/layerproto/transactions.proto';
import jsonProtoDefsWl from 'WaylandSafePath/waylandtrace.proto';
import jsonProtoDefsSysUi from 'frameworks/base/packages/SystemUI/src/com/android/systemui/tracing/sysui_trace.proto';
import jsonProtoDefsLauncher from 'packages/apps/Launcher3/protos/launcher_trace_file.proto';
import jsonProtoDefsIme from 'frameworks/base/core/proto/android/view/inputmethod/inputmethodeditortrace.proto';
import jsonProtoDefsTags from 'platform_testing/libraries/flicker/src/com/android/server/wm/proto/tags.proto';
import jsonProtoDefsErrors from 'platform_testing/libraries/flicker/src/com/android/server/wm/proto/errors.proto';
import protobuf from 'protobufjs';
import {transform_accessibility_trace} from './transform_accessibility.js';
import {transform_transaction_trace} from './transform_transaction.js';
import {transform_wl_outputstate, transform_wayland_trace} from './transform_wl.js';
import {transformProtolog} from './transform_protolog.js';
import {transform_sysui_trace} from './transform_sys_ui.js';
import {transform_launcher_trace} from './transform_launcher.js';
import {transform_ime_trace_clients, transform_ime_trace_service, transform_ime_trace_managerservice} from './transform_ime.js';
import {mp4Decoder} from './decodeVideo.js';
import AccessibilityTrace from '@/traces/Accessibility.ts';
import SurfaceFlingerTrace from '@/traces/SurfaceFlinger.ts';
import WindowManagerTrace from '@/traces/WindowManager.ts';
import TransactionsTrace from '@/traces/Transactions.ts';
import TransactionsTraceLegacy from '@/traces/TransactionsLegacy.ts';
import ScreenRecordingTrace from '@/traces/ScreenRecording.ts';
import WaylandTrace from '@/traces/Wayland.ts';
import ProtoLogTrace from '@/traces/ProtoLog.ts';
import SystemUITrace from '@/traces/SystemUI.ts';
import LauncherTrace from '@/traces/Launcher.ts';
import ImeTraceClients from '@/traces/InputMethodClients.ts';
import ImeTraceService from '@/traces/InputMethodService.ts';
import ImeTraceManagerService from '@/traces/InputMethodManagerService.ts';
import SurfaceFlingerDump from '@/dumps/SurfaceFlinger.ts';
import WindowManagerDump from '@/dumps/WindowManager.ts';
import WaylandDump from '@/dumps/Wayland.ts';
import TagTrace from '@/traces/TraceTag.ts';
import ErrorTrace from '@/traces/TraceError.ts';
const AccessibilityTraceMessage = lookup_type(jsonProtoDefsAccessibility, 'com.android.server.accessibility.AccessibilityTraceFileProto');
const WmTraceMessage = lookup_type(jsonProtoDefsWm, 'com.android.server.wm.WindowManagerTraceFileProto');
const WmDumpMessage = lookup_type(jsonProtoDefsWm, 'com.android.server.wm.WindowManagerServiceDumpProto');
const SfTraceMessage = lookup_type(jsonProtoDefsSf, 'android.surfaceflinger.LayersTraceFileProto');
const SfDumpMessage = lookup_type(jsonProtoDefsSf, 'android.surfaceflinger.LayersProto');
const SfTransactionTraceMessage = lookup_type(jsonProtoDefsTransaction, 'TransactionTraceFile');
const WaylandTraceMessage = lookup_type(jsonProtoDefsWl, 'org.chromium.arc.wayland_composer.TraceFileProto');
const WaylandDumpMessage = lookup_type(jsonProtoDefsWl, 'org.chromium.arc.wayland_composer.OutputStateProto');
const ProtoLogMessage = lookup_type(jsonProtoDefsProtoLog, 'com.android.internal.protolog.ProtoLogFileProto');
const SystemUiTraceMessage = lookup_type(jsonProtoDefsSysUi, 'com.android.systemui.tracing.SystemUiTraceFileProto');
const LauncherTraceMessage = lookup_type(jsonProtoDefsLauncher, 'com.android.launcher3.tracing.LauncherTraceFileProto');
const InputMethodClientsTraceMessage = lookup_type(jsonProtoDefsIme, 'android.view.inputmethod.InputMethodClientsTraceFileProto');
const InputMethodServiceTraceMessage = lookup_type(jsonProtoDefsIme, 'android.view.inputmethod.InputMethodServiceTraceFileProto');
const InputMethodManagerServiceTraceMessage = lookup_type(jsonProtoDefsIme, 'android.view.inputmethod.InputMethodManagerServiceTraceFileProto');
const TagTraceMessage = lookup_type(jsonProtoDefsTags, 'com.android.server.wm.flicker.FlickerTagTraceProto');
const ErrorTraceMessage = lookup_type(jsonProtoDefsErrors, 'com.android.server.wm.flicker.FlickerErrorTraceProto');
const ACCESSIBILITY_MAGIC_NUMBER = [0x09, 0x41, 0x31, 0x31, 0x59, 0x54, 0x52, 0x41, 0x43]; // .A11YTRAC
const LAYER_TRACE_MAGIC_NUMBER = [0x09, 0x4c, 0x59, 0x52, 0x54, 0x52, 0x41, 0x43, 0x45]; // .LYRTRACE
const TRANSACTIONS_TRACE_MAGIC_NUMBER = [0x09, 0x54, 0x4e, 0x58, 0x54, 0x52, 0x41, 0x43, 0x45]; // .TNXTRACE
const WINDOW_TRACE_MAGIC_NUMBER = [0x09, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45]; // .WINTRACE
const MPEG4_MAGIC_NMBER = [0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32]; // ....ftypmp42
const WAYLAND_TRACE_MAGIC_NUMBER = [0x09, 0x57, 0x59, 0x4c, 0x54, 0x52, 0x41, 0x43, 0x45]; // .WYLTRACE
const PROTO_LOG_MAGIC_NUMBER = [0x09, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x4c, 0x4f, 0x47]; // .PROTOLOG
const SYSTEM_UI_MAGIC_NUMBER = [0x09, 0x53, 0x59, 0x53, 0x55, 0x49, 0x54, 0x52, 0x43]; // .SYSUITRC
const LAUNCHER_MAGIC_NUMBER = [0x09, 0x4C, 0x4E, 0x43, 0x48, 0x52, 0x54, 0x52, 0x43]; // .LNCHRTRC
const IMC_TRACE_MAGIC_NUMBER = [0x09, 0x49, 0x4d, 0x43, 0x54, 0x52, 0x41, 0x43, 0x45]; // .IMCTRACE
const IMS_TRACE_MAGIC_NUMBER = [0x09, 0x49, 0x4d, 0x53, 0x54, 0x52, 0x41, 0x43, 0x45]; // .IMSTRACE
const IMM_TRACE_MAGIC_NUMBER = [0x09, 0x49, 0x4d, 0x4d, 0x54, 0x52, 0x41, 0x43, 0x45]; // .IMMTRACE
const TAG_TRACE_MAGIC_NUMBER = [0x09, 0x54, 0x41, 0x47, 0x54, 0x52, 0x41, 0x43, 0x45]; //.TAGTRACE
const ERROR_TRACE_MAGIC_NUMBER = [0x09, 0x45, 0x52, 0x52, 0x54, 0x52, 0x41, 0x43, 0x45]; //.ERRORTRACE
const FILE_TYPES = Object.freeze({
ACCESSIBILITY_TRACE: 'AccessibilityTrace',
WINDOW_MANAGER_TRACE: 'WindowManagerTrace',
SURFACE_FLINGER_TRACE: 'SurfaceFlingerTrace',
WINDOW_MANAGER_DUMP: 'WindowManagerDump',
SURFACE_FLINGER_DUMP: 'SurfaceFlingerDump',
SCREEN_RECORDING: 'ScreenRecording',
TRANSACTIONS_TRACE: 'TransactionsTrace',
TRANSACTIONS_TRACE_LEGACY: 'TransactionsTraceLegacy',
WAYLAND_TRACE: 'WaylandTrace',
WAYLAND_DUMP: 'WaylandDump',
PROTO_LOG: 'ProtoLog',
SYSTEM_UI: 'SystemUI',
LAUNCHER: 'Launcher',
IME_TRACE_CLIENTS: 'ImeTraceClients',
IME_TRACE_SERVICE: 'ImeTrace InputMethodService',
IME_TRACE_MANAGERSERVICE: 'ImeTrace InputMethodManagerService',
TAG_TRACE: 'TagTrace',
ERROR_TRACE: 'ErrorTrace',
});
const WINDOW_MANAGER_ICON = 'view_compact';
const SURFACE_FLINGER_ICON = 'filter_none';
const SCREEN_RECORDING_ICON = 'videocam';
const TRANSACTION_ICON = 'timeline';
const WAYLAND_ICON = 'filter_none';
const PROTO_LOG_ICON = 'notes';
const SYSTEM_UI_ICON = 'filter_none';
const LAUNCHER_ICON = 'filter_none';
const IME_ICON = 'keyboard';
const ACCESSIBILITY_ICON = 'filter_none';
const TAG_ICON = 'details';
const TRACE_ERROR_ICON = 'warning';
const FILE_ICONS = {
[FILE_TYPES.ACCESSIBILITY_TRACE]: ACCESSIBILITY_ICON,
[FILE_TYPES.WINDOW_MANAGER_TRACE]: WINDOW_MANAGER_ICON,
[FILE_TYPES.SURFACE_FLINGER_TRACE]: SURFACE_FLINGER_ICON,
[FILE_TYPES.WINDOW_MANAGER_DUMP]: WINDOW_MANAGER_ICON,
[FILE_TYPES.SURFACE_FLINGER_DUMP]: SURFACE_FLINGER_ICON,
[FILE_TYPES.SCREEN_RECORDING]: SCREEN_RECORDING_ICON,
[FILE_TYPES.TRANSACTIONS_TRACE]: TRANSACTION_ICON,
[FILE_TYPES.TRANSACTIONS_TRACE_LEGACY]: TRANSACTION_ICON,
[FILE_TYPES.WAYLAND_TRACE]: WAYLAND_ICON,
[FILE_TYPES.WAYLAND_DUMP]: WAYLAND_ICON,
[FILE_TYPES.PROTO_LOG]: PROTO_LOG_ICON,
[FILE_TYPES.SYSTEM_UI]: SYSTEM_UI_ICON,
[FILE_TYPES.LAUNCHER]: LAUNCHER_ICON,
[FILE_TYPES.IME_TRACE_CLIENTS]: IME_ICON,
[FILE_TYPES.IME_TRACE_SERVICE]: IME_ICON,
[FILE_TYPES.IME_TRACE_MANAGERSERVICE]: IME_ICON,
[FILE_TYPES.TAG_TRACE]: TAG_ICON,
[FILE_TYPES.ERROR_TRACE]: TRACE_ERROR_ICON,
};
function oneOf(dataType) {
return {oneOf: true, type: dataType};
}
const TRACE_TYPES = Object.freeze({
ACCESSIBILITY: 'AccessibilityTrace',
WINDOW_MANAGER: 'WindowManagerTrace',
SURFACE_FLINGER: 'SurfaceFlingerTrace',
SCREEN_RECORDING: 'ScreenRecording',
TRANSACTION: 'Transaction',
TRANSACTION_LEGACY: 'Transaction (Legacy)',
WAYLAND: 'Wayland',
PROTO_LOG: 'ProtoLog',
SYSTEM_UI: 'SystemUI',
LAUNCHER: 'Launcher',
IME_CLIENTS: 'ImeTrace Clients',
IME_SERVICE: 'ImeTrace InputMethodService',
IME_MANAGERSERVICE: 'ImeTrace InputMethodManagerService',
TAG: 'TagTrace',
ERROR: 'ErrorTrace',
});
const TRACE_INFO = {
[TRACE_TYPES.ACCESSIBILITY]: {
name: 'Accessibility',
icon: ACCESSIBILITY_ICON,
files: [oneOf(FILE_TYPES.ACCESSIBILITY_TRACE)],
constructor: AccessibilityTrace,
},
[TRACE_TYPES.WINDOW_MANAGER]: {
name: 'WindowManager',
icon: WINDOW_MANAGER_ICON,
files: [oneOf(FILE_TYPES.WINDOW_MANAGER_TRACE)],
constructor: WindowManagerTrace,
},
[TRACE_TYPES.SURFACE_FLINGER]: {
name: 'SurfaceFlinger',
icon: SURFACE_FLINGER_ICON,
files: [oneOf(FILE_TYPES.SURFACE_FLINGER_TRACE)],
constructor: SurfaceFlingerTrace,
},
[TRACE_TYPES.SCREEN_RECORDING]: {
name: 'Screen recording',
icon: SCREEN_RECORDING_ICON,
files: [oneOf(FILE_TYPES.SCREEN_RECORDING)],
constructor: ScreenRecordingTrace,
},
[TRACE_TYPES.TRANSACTION]: {
name: 'Transaction',
icon: TRANSACTION_ICON,
files: [
oneOf(FILE_TYPES.TRANSACTIONS_TRACE),
],
constructor: TransactionsTrace,
},
[TRACE_TYPES.TRANSACTION_LEGACY]: {
name: 'Transactions (Legacy)',
icon: TRANSACTION_ICON,
files: [
oneOf(FILE_TYPES.TRANSACTIONS_TRACE_LEGACY),
],
constructor: TransactionsTraceLegacy,
},
[TRACE_TYPES.WAYLAND]: {
name: 'Wayland',
icon: WAYLAND_ICON,
files: [oneOf(FILE_TYPES.WAYLAND_TRACE)],
constructor: WaylandTrace,
},
[TRACE_TYPES.PROTO_LOG]: {
name: 'ProtoLog',
icon: PROTO_LOG_ICON,
files: [oneOf(FILE_TYPES.PROTO_LOG)],
constructor: ProtoLogTrace,
},
[TRACE_TYPES.SYSTEM_UI]: {
name: 'SystemUI',
icon: SYSTEM_UI_ICON,
files: [oneOf(FILE_TYPES.SYSTEM_UI)],
constructor: SystemUITrace,
},
[TRACE_TYPES.LAUNCHER]: {
name: 'Launcher',
icon: LAUNCHER_ICON,
files: [oneOf(FILE_TYPES.LAUNCHER)],
constructor: LauncherTrace,
},
[TRACE_TYPES.IME_CLIENTS]: {
name: 'InputMethodClients',
icon: IME_ICON,
files: [oneOf(FILE_TYPES.IME_TRACE_CLIENTS)],
constructor: ImeTraceClients,
},
[TRACE_TYPES.IME_SERVICE]: {
name: 'InputMethodService',
icon: IME_ICON,
files: [oneOf(FILE_TYPES.IME_TRACE_SERVICE)],
constructor: ImeTraceService,
},
[TRACE_TYPES.IME_MANAGERSERVICE]: {
name: 'InputMethodManagerService',
icon: IME_ICON,
files: [oneOf(FILE_TYPES.IME_TRACE_MANAGERSERVICE)],
constructor: ImeTraceManagerService,
},
[TRACE_TYPES.TAG]: {
name: 'Tag',
icon: TAG_ICON,
files: [oneOf(FILE_TYPES.TAG_TRACE)],
constructor: TagTrace,
},
[TRACE_TYPES.ERROR]: {
name: 'Error',
icon: TRACE_ERROR_ICON,
files: [oneOf(FILE_TYPES.ERROR_TRACE)],
constructor: ErrorTrace,
},
};
const DUMP_TYPES = Object.freeze({
WINDOW_MANAGER: 'WindowManagerDump',
SURFACE_FLINGER: 'SurfaceFlingerDump',
WAYLAND: 'WaylandDump',
});
const DUMP_INFO = {
[DUMP_TYPES.WINDOW_MANAGER]: {
name: 'WindowManager',
icon: WINDOW_MANAGER_ICON,
files: [oneOf(FILE_TYPES.WINDOW_MANAGER_DUMP)],
constructor: WindowManagerDump,
},
[DUMP_TYPES.SURFACE_FLINGER]: {
name: 'SurfaceFlinger',
icon: SURFACE_FLINGER_ICON,
files: [oneOf(FILE_TYPES.SURFACE_FLINGER_DUMP)],
constructor: SurfaceFlingerDump,
},
[DUMP_TYPES.WAYLAND]: {
name: 'Wayland',
icon: WAYLAND_ICON,
files: [oneOf(FILE_TYPES.WAYLAND_DUMP)],
constructor: WaylandDump,
},
};
export const TRACE_ICONS = {
[TRACE_TYPES.WINDOW_MANAGER]: WINDOW_MANAGER_ICON,
[TRACE_TYPES.SURFACE_FLINGER]: SURFACE_FLINGER_ICON,
[TRACE_TYPES.SCREEN_RECORDING]: SCREEN_RECORDING_ICON,
[TRACE_TYPES.TRANSACTION]: TRANSACTION_ICON,
[TRACE_TYPES.TRANSACTION_LEGACY]: TRANSACTION_ICON,
[TRACE_TYPES.WAYLAND]: WAYLAND_ICON,
[TRACE_TYPES.PROTO_LOG]: PROTO_LOG_ICON,
[TRACE_TYPES.SYSTEM_UI]: SYSTEM_UI_ICON,
[TRACE_TYPES.LAUNCHER]: LAUNCHER_ICON,
[TRACE_TYPES.IME_CLIENTS]: IME_ICON,
[TRACE_TYPES.IME_SERVICE]: IME_ICON,
[TRACE_TYPES.IME_MANAGERSERVICE]: IME_ICON,
[TRACE_TYPES.TAG_TRACE]: TAG_ICON,
[TRACE_TYPES.ERROR_TRACE]: TRACE_ERROR_ICON,
[DUMP_TYPES.WINDOW_MANAGER]: WINDOW_MANAGER_ICON,
[DUMP_TYPES.SURFACE_FLINGER]: SURFACE_FLINGER_ICON,
[DUMP_TYPES.WAYLAND]: WAYLAND_ICON,
};
// TODO: Rename name to defaultName
const FILE_DECODERS = {
[FILE_TYPES.ACCESSIBILITY_TRACE]: {
name: 'Accessibility trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.ACCESSIBILITY_TRACE,
objTypeProto: AccessibilityTraceMessage,
transform: transform_accessibility_trace,
timeline: true,
},
},
[FILE_TYPES.WINDOW_MANAGER_TRACE]: {
name: 'WindowManager trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.WINDOW_MANAGER_TRACE,
objTypeProto: WmTraceMessage,
transform: WindowManagerTrace.fromProto,
timeline: true,
},
},
[FILE_TYPES.SURFACE_FLINGER_TRACE]: {
name: 'SurfaceFlinger trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.SURFACE_FLINGER_TRACE,
mime: 'application/octet-stream',
objTypeProto: SfTraceMessage,
transform: SurfaceFlingerTrace.fromProto,
timeline: true,
},
},
[FILE_TYPES.WAYLAND_TRACE]: {
name: 'Wayland trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.WAYLAND_TRACE,
mime: 'application/octet-stream',
objTypeProto: WaylandTraceMessage,
transform: transform_wayland_trace,
timeline: true,
},
},
[FILE_TYPES.SURFACE_FLINGER_DUMP]: {
name: 'SurfaceFlinger dump',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.SURFACE_FLINGER_DUMP,
mime: 'application/octet-stream',
objTypeProto: [SfDumpMessage, SfTraceMessage],
transform: [SurfaceFlingerDump.fromProto, SurfaceFlingerTrace.fromProto],
timeline: true,
},
},
[FILE_TYPES.WINDOW_MANAGER_DUMP]: {
name: 'WindowManager dump',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.WINDOW_MANAGER_DUMP,
mime: 'application/octet-stream',
objTypeProto: WmDumpMessage,
transform: WindowManagerDump.fromProto,
timeline: true,
},
},
[FILE_TYPES.WAYLAND_DUMP]: {
name: 'Wayland dump',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.WAYLAND_DUMP,
mime: 'application/octet-stream',
objTypeProto: WaylandDumpMessage,
transform: transform_wl_outputstate,
timeline: true,
},
},
[FILE_TYPES.SCREEN_RECORDING]: {
name: 'Screen recording',
decoder: videoDecoder,
decoderParams: {
type: FILE_TYPES.SCREEN_RECORDING,
mime: 'video/mp4',
videoDecoder: mp4Decoder,
},
},
[FILE_TYPES.TRANSACTIONS_TRACE]: {
name: 'Transaction',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.TRANSACTIONS_TRACE,
mime: 'application/octet-stream',
objTypeProto: SfTransactionTraceMessage,
transform: transform_transaction_trace,
timeline: true,
},
},
[FILE_TYPES.PROTO_LOG]: {
name: 'ProtoLog',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.PROTO_LOG,
mime: 'application/octet-stream',
objTypeProto: ProtoLogMessage,
transform: transformProtolog,
timeline: true,
},
},
[FILE_TYPES.SYSTEM_UI]: {
name: 'SystemUI trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.SYSTEM_UI,
mime: 'application/octet-stream',
objTypeProto: SystemUiTraceMessage,
transform: transform_sysui_trace,
timeline: true,
},
},
[FILE_TYPES.LAUNCHER]: {
name: 'Launcher trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.LAUNCHER,
mime: 'application/octet-stream',
objTypeProto: LauncherTraceMessage,
transform: transform_launcher_trace,
timeline: true,
},
},
[FILE_TYPES.IME_TRACE_CLIENTS]: {
name: 'InputMethodClients trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.IME_TRACE_CLIENTS,
mime: 'application/octet-stream',
objTypeProto: InputMethodClientsTraceMessage,
transform: transform_ime_trace_clients,
timeline: true,
},
},
[FILE_TYPES.IME_TRACE_SERVICE]: {
name: 'InputMethodService trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.IME_TRACE_SERVICE,
mime: 'application/octet-stream',
objTypeProto: InputMethodServiceTraceMessage,
transform: transform_ime_trace_service,
timeline: true,
},
},
[FILE_TYPES.IME_TRACE_MANAGERSERVICE]: {
name: 'InputMethodManagerService trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.IME_TRACE_MANAGERSERVICE,
mime: 'application/octet-stream',
objTypeProto: InputMethodManagerServiceTraceMessage,
transform: transform_ime_trace_managerservice,
timeline: true,
},
},
[FILE_TYPES.TAG_TRACE]: {
name: 'Tag trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.TAG_TRACE,
objTypeProto: TagTraceMessage,
transform: TagTrace.fromProto,
timeline: true,
},
},
[FILE_TYPES.ERROR_TRACE]: {
name: 'Error trace',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.ERROR_TRACE,
objTypeProto: ErrorTraceMessage,
transform: ErrorTrace.fromProto,
timeline: true,
},
},
};
function lookup_type(protoPath, type) {
return protobuf.Root.fromJSON(protoPath).lookupType(type);
}
// Replace enum values with string representation and
// add default values to the proto objects. This function also handles
// a special case with TransformProtos where the matrix may be derived
// from the transform type.
function modifyProtoFields(protoObj, displayDefaults) {
if (!protoObj || protoObj !== Object(protoObj) || !protoObj.$type) {
return;
}
for (const fieldName in protoObj.$type.fields) {
if (protoObj.$type.fields.hasOwnProperty(fieldName)) {
const fieldProperties = protoObj.$type.fields[fieldName];
const field = protoObj[fieldName];
if (Array.isArray(field)) {
field.forEach((item, _) => {
modifyProtoFields(item, displayDefaults);
});
continue;
}
if (displayDefaults && !(field)) {
protoObj[fieldName] = fieldProperties.defaultValue;
}
if (fieldProperties.resolvedType && fieldProperties.resolvedType.valuesById) {
protoObj[fieldName] = fieldProperties.resolvedType.valuesById[protoObj[fieldProperties.name]];
continue;
}
modifyProtoFields(protoObj[fieldName], displayDefaults);
}
}
}
function decodeAndTransformProto(buffer, params, displayDefaults) {
var objTypesProto = [];
var transforms = [];
if (!Array.isArray(params.objTypeProto)) {
objTypesProto = [params.objTypeProto];
transforms = [params.transform];
} else {
objTypesProto = params.objTypeProto;
transforms = params.transform;
}
// each trace or dump may have different processors, for example, until S, SF dumps
// returne a list of layers and winscope built a [LayerTraceEntry] from them.
// From S onwards, returns a LayerTrace object, iterating over multiple items allows
// winscope to handle both the new and legacy formats
// TODO Refactor the decode.js code into a set of decoders to clean up the code
let lastError = null;
for (var x = 0; x < objTypesProto.length; x++) {
const objType = objTypesProto[x];
const transform = transforms[x];
try {
const decoded = objType.decode(buffer);
modifyProtoFields(decoded, displayDefaults);
const transformed = transform(decoded);
return transformed;
} catch (e) {
lastError = e;
// check next parser
}
}
if (lastError) {
throw lastError;
}
throw new UndetectableFileType('Unable to parse file');
}
function protoDecoder(buffer, params, fileName, store) {
const transformed = decodeAndTransformProto(buffer, params, store.displayDefaults);
// add tagGenerationTrace to dataFile for WM/SF traces so tags can be generated
var tagGenerationTrace = null;
if (params.type === FILE_TYPES.WINDOW_MANAGER_TRACE ||
params.type === FILE_TYPES.SURFACE_FLINGER_TRACE) {
tagGenerationTrace = transformed;
}
let data;
if (params.timeline) {
data = transformed.entries ?? transformed.children;
} else {
data = [transformed];
}
const blobUrl = URL.createObjectURL(new Blob([buffer], {type: params.mime}));
return dataFile(
fileName,
data.map((x) => x.timestamp),
data,
blobUrl,
params.type,
tagGenerationTrace
);
}
function videoDecoder(buffer, params, fileName, store) {
const [data, timeline] = params.videoDecoder(buffer);
const blobUrl = URL.createObjectURL(new Blob([data], {type: params.mime}));
return dataFile(fileName, timeline, blobUrl, blobUrl, params.type);
}
function dataFile(filename, timeline, data, blobUrl, type, tagGenerationTrace = null) {
return {
filename: filename,
// Object is frozen for performance reasons
// It will prevent Vue from making it a reactive object which will be very slow as the timeline gets larger.
timeline: Object.freeze(timeline),
data: data,
blobUrl: blobUrl,
tagGenerationTrace: tagGenerationTrace,
type: type,
selectedIndex: 0,
destroy() {
URL.revokeObjectURL(this.blobUrl);
},
};
}
function arrayEquals(a, b) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
function arrayStartsWith(array, prefix) {
return arrayEquals(array.slice(0, prefix.length), prefix);
}
function decodedFile(fileType, buffer, fileName, store) {
const fileDecoder = FILE_DECODERS[fileType];
return [fileType, fileDecoder.decoder(buffer, fileDecoder.decoderParams, fileName, store)];
}
function detectAndDecode(buffer, fileName, store) {
if (arrayStartsWith(buffer, LAYER_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.SURFACE_FLINGER_TRACE, buffer, fileName, store);
}
if (arrayStartsWith(buffer, ACCESSIBILITY_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.ACCESSIBILITY_TRACE, buffer, fileName, store);
}
if (arrayStartsWith(buffer, WINDOW_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.WINDOW_MANAGER_TRACE, buffer, fileName, store);
}
if (arrayStartsWith(buffer, MPEG4_MAGIC_NMBER)) {
return decodedFile(FILE_TYPES.SCREEN_RECORDING, buffer, fileName, store);
}
if (arrayStartsWith(buffer, TRANSACTIONS_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.TRANSACTIONS_TRACE, buffer, fileName, store);
}
if (arrayStartsWith(buffer, WAYLAND_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.WAYLAND_TRACE, buffer, fileName, store);
}
if (arrayStartsWith(buffer, PROTO_LOG_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.PROTO_LOG, buffer, fileName, store);
}
if (arrayStartsWith(buffer, SYSTEM_UI_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.SYSTEM_UI, buffer, fileName, store);
}
if (arrayStartsWith(buffer, LAUNCHER_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.LAUNCHER, buffer, fileName, store);
}
if (arrayStartsWith(buffer, IMC_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.IME_TRACE_CLIENTS, buffer, fileName, store);
}
if (arrayStartsWith(buffer, IMS_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.IME_TRACE_SERVICE, buffer, fileName, store);
}
if (arrayStartsWith(buffer, IMM_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.IME_TRACE_MANAGERSERVICE, buffer, fileName, store);
}
if (arrayStartsWith(buffer, TAG_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.TAG_TRACE, buffer, fileName, store);
}
if (arrayStartsWith(buffer, ERROR_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.ERROR_TRACE, buffer, fileName, store);
}
// TODO(b/169305853): Add magic number at beginning of file for better auto detection
for (const [filetype, condition] of [
[FILE_TYPES.TRANSACTIONS_TRACE_LEGACY, (file) => file.data.length > 0],
[FILE_TYPES.WAYLAND_DUMP, (file) => (file.data.length > 0 && file.data.children[0] > 0) || file.data.length > 1],
[FILE_TYPES.WINDOW_MANAGER_DUMP],
[FILE_TYPES.SURFACE_FLINGER_DUMP]
]) {
try {
const [, fileData] = decodedFile(filetype, buffer, fileName, store);
// A generic file will often wrongly be decoded as an empty wayland dump file
if (condition && !condition(fileData)) {
// Fall through to next filetype
continue;
}
return [filetype, fileData];
} catch (ex) {
// ignore exception and fall through to next filetype
}
}
throw new UndetectableFileType('Unable to detect file');
}
/**
* Error is raised when detectAndDecode is called but the file can't be
* automatically detected as being of a compatible file type.
*/
class UndetectableFileType extends Error { }
export {
dataFile,
detectAndDecode,
decodeAndTransformProto,
TagTraceMessage,
FILE_TYPES,
TRACE_INFO,
TRACE_TYPES,
DUMP_TYPES,
DUMP_INFO,
FILE_DECODERS,
FILE_ICONS,
UndetectableFileType
};

View File

@@ -1,69 +0,0 @@
/*
* Copyright 2017, 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.
*/
const WINSCOPE_META_MAGIC_STRING = [0x23, 0x56, 0x56, 0x31, 0x4e, 0x53, 0x43, 0x30, 0x50, 0x45, 0x54, 0x31, 0x4d, 0x45, 0x21, 0x23]; // #VV1NSC0PET1ME!#
// Suitable only for short patterns
function findFirstInArray(array, pattern) {
for (var i = 0; i < array.length; i++) {
var match = true;
for (var j = 0; j < pattern.length; j++) {
if (array[i + j] != pattern[j]) {
match = false;
break;
}
}
if (match) {
return i;
}
}
return -1;
}
function parseUintNLE(buffer, position, bytes) {
var num = 0;
for (var i = bytes - 1; i >= 0; i--) {
num = num * 256
num += buffer[position + i];
}
return num;
}
function parseUint32LE(buffer, position) {
return parseUintNLE(buffer, position, 4)
}
function parseUint64LE(buffer, position) {
return parseUintNLE(buffer, position, 8)
}
function mp4Decoder(buffer) {
var dataStart = findFirstInArray(buffer, WINSCOPE_META_MAGIC_STRING);
if (dataStart < 0) {
throw new Error('Unable to find sync metadata in the file. Are you using the latest Android ScreenRecorder version?');
}
dataStart += WINSCOPE_META_MAGIC_STRING.length;
var frameNum = parseUint32LE(buffer, dataStart);
dataStart += 4;
var timeline = [];
for (var i = 0; i < frameNum; i++) {
timeline.push(parseUint64LE(buffer, dataStart) * 1000);
dataStart += 8;
}
return [buffer, timeline]
}
export { mp4Decoder };

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2020, 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.
*/
export default abstract class DumpBase implements IDump {
data: any;
_files: any[];
constructor(data, files) {
this.data = data;
this._files = files;
}
get files(): readonly any[] {
return Object.values(this._files).flat();
}
abstract get type(): String;
}
interface IDump {
files: readonly Object[];
type: String,
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2020, 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 { FILE_TYPES, DUMP_TYPES } from "@/decode.js";
import DumpBase from "./DumpBase";
import LayersTraceEntry from '../flickerlib/layers/LayerTraceEntry';
import LayersTrace from '../flickerlib/LayersTrace';
export default class SurfaceFlinger extends DumpBase {
sfDumpFile: any;
data: any;
constructor(files) {
const sfDumpFile = files[FILE_TYPES.SURFACE_FLINGER_DUMP];
super(sfDumpFile.data, files);
this.sfDumpFile = sfDumpFile;
}
get type() {
return DUMP_TYPES.SURFACE_FLINGER;
}
static fromProto(proto: any): LayersTrace {
const source = null;
const entry = LayersTraceEntry.fromProto(
/* protos */ proto.layers,
/* displays */ proto.displays,
/* timestamp */ 0,
/* hwcBlob */ ""
);
return new LayersTrace([entry], source);
}
}

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2020, 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 { FILE_TYPES, DUMP_TYPES } from "@/decode.js";
import DumpBase from "./DumpBase";
export default class WayLand extends DumpBase {
waylandFile: any;
constructor(files) {
const waylandFile = files[FILE_TYPES.WAYLAND_DUMP];
super(waylandFile.data, files);
this.waylandFile = waylandFile;
}
get type() {
return DUMP_TYPES.WAYLAND;
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2020, 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 { FILE_TYPES, DUMP_TYPES } from "@/decode.js";
import DumpBase from "./DumpBase";
import { WindowManagerTrace } from '@/flickerlib';
export default class WindowManager extends DumpBase {
wmDumpFile: any;
constructor(files) {
const wmDumpFile = files[FILE_TYPES.WINDOW_MANAGER_DUMP];
super(wmDumpFile.data, files);
this.wmDumpFile = wmDumpFile
}
get type() {
return DUMP_TYPES.WINDOW_MANAGER;
}
static fromProto(proto: any): WindowManagerTrace {
const source = null;
const state = WindowManagerTrace.fromDump(proto);
return new WindowManagerTrace([state], source);
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2020, 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 { ErrorTrace } from "./common"
import ErrorState from "./errors/ErrorState"
ErrorTrace.fromProto = function (proto: any) {
const states = [];
for (const stateProto of proto.states) {
const transformedState = ErrorState.fromProto(
stateProto.errors,
stateProto.timestamp);
states.push(transformedState);
}
const source = null;
return new ErrorTrace(states, source);
}
export default ErrorTrace;

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2020, 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 { LayersTrace } from "./common"
import LayerTraceEntryLazy from './layers/LayerTraceEntryLazy'
LayersTrace.fromProto = function (proto: any): LayersTrace {
const entries = []
for (const entryProto of proto.entry) {
const transformedEntry = new LayerTraceEntryLazy(
/* protos */ entryProto.layers.layers,
/* displays */ entryProto.displays,
/* timestamp */ entryProto.elapsedRealtimeNanos,
/* hwcBlob */ entryProto.hwcBlob);
entries.push(transformedEntry);
}
const source = null;
const trace = new LayersTrace(entries, source);
return trace;
}
export default LayersTrace;

View File

@@ -1,278 +0,0 @@
/*
* Copyright 2021, 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 {toSize, toActiveBuffer, toColor, toColor3, toPoint, toRect,
toRectF, toRegion, toMatrix22, toTransform} from './common';
import intDefMapping from
'../../../../../prebuilts/misc/common/winscope/intDefMapping.json';
import config from '../config/Configuration.json'
function readIntdefMap(): Map<string, string> {
const map = new Map<string, string>();
const keys = Object.keys(config.intDefColumn);
keys.forEach(key => {
const value = config.intDefColumn[key];
map.set(key, value);
});
return map;
}
export default class ObjectFormatter {
static displayDefaults: boolean = false
private static INVALID_ELEMENT_PROPERTIES = config.invalidProperties;
private static FLICKER_INTDEF_MAP = readIntdefMap();
static cloneObject(entry: any): any {
let obj: any = {}
const properties = ObjectFormatter.getProperties(entry);
properties.forEach(prop => obj[prop] = entry[prop]);
return obj;
}
/**
* Get the true properties of an entry excluding functions, kotlin gernerated
* variables, explicitly excluded properties, and flicker objects already in
* the hierarchy that shouldn't be traversed when formatting the entry
* @param entry The entry for which we want to get the properties for
* @return The "true" properties of the entry as described above
*/
static getProperties(entry: any): string[] {
var props = [];
let obj = entry;
do {
const properties = Object.getOwnPropertyNames(obj).filter(it => {
// filter out functions
if (typeof(entry[it]) === 'function') return false;
// internal propertires from kotlinJs
if (it.includes(`$`)) return false;
// private kotlin variables from kotlin
if (it.startsWith(`_`)) return false;
// some predefined properties used only internally (e.g., children, ref, diff)
if (this.INVALID_ELEMENT_PROPERTIES.includes(it)) return false;
const value = entry[it];
// only non-empty arrays of non-flicker objects (otherwise they are in hierarchy)
if (Array.isArray(value) && value.length > 0) return !value[0].stableId;
// non-flicker object
return !(value?.stableId);
});
properties.forEach(function (prop) {
if (typeof(entry[prop]) !== 'function' && props.indexOf(prop) === -1) {
props.push(prop);
}
});
} while (obj = Object.getPrototypeOf(obj));
return props;
}
/**
* Format a Winscope entry to be displayed in the UI
* Accounts for different user display settings (e.g. hiding empty/default values)
* @param obj The raw object to format
* @return The formatted object
*/
static format(obj: any): {} {
const properties = this.getProperties(obj);
const sortedProperties = properties.sort()
const result: any = {}
sortedProperties.forEach(entry => {
const key = entry;
const value: any = obj[key];
if (value === null || value === undefined) {
if (this.displayDefaults) {
result[key] = value;
}
return
}
if (value || this.displayDefaults) {
// raw values (e.g., false or 0)
if (!value) {
result[key] = value
// flicker obj
} else if (value.prettyPrint) {
const isEmpty = value.isEmpty === true;
if (!isEmpty || this.displayDefaults) {
result[key] = value.prettyPrint();
}
} else {
// converted proto to flicker
const translatedObject = this.translateObject(value);
if (translatedObject) {
if (translatedObject.prettyPrint) {
result[key] = translatedObject.prettyPrint();
}
else {
result[key] = translatedObject;
}
// objects - recursive call
} else if (value && typeof(value) == `object`) {
const childObj = this.format(value) as any;
const isEmpty = Object.entries(childObj).length == 0 || childObj.isEmpty;
if (!isEmpty || this.displayDefaults) {
result[key] = childObj;
}
} else {
// values
result[key] = this.translateIntDef(obj, key, value);
}
}
}
})
return result;
}
/**
* Translate some predetermined proto objects into their flicker equivalent
*
* Returns null if the object cannot be translated
*
* @param obj Object to translate
*/
private static translateObject(obj) {
const type = obj?.$type?.name;
switch(type) {
case `SizeProto`: return toSize(obj);
case `ActiveBufferProto`: return toActiveBuffer(obj);
case `Color3`: return toColor3(obj);
case `ColorProto`: return toColor(obj);
case `PointProto`: return toPoint(obj);
case `RectProto`: return toRect(obj);
case `Matrix22`: return toMatrix22(obj);
case `FloatRectProto`: return toRectF(obj);
case `RegionProto`: return toRegion(obj);
case `TransformProto`: return toTransform(obj);
case 'ColorTransformProto': {
const formatted = this.formatColorTransform(obj.val);
return `${formatted}`;
}
}
return null;
}
private static formatColorTransform(vals) {
const fixedVals = vals.map((v) => v.toFixed(1));
let formatted = ``;
for (let i = 0; i < fixedVals.length; i += 4) {
formatted += `[`;
formatted += fixedVals.slice(i, i + 4).join(', ');
formatted += `] `;
}
return formatted;
}
/**
* Obtains from the proto field, the metadata related to the typedef type (if any)
*
* @param obj Proto object
* @param propertyName Property to search
*/
private static getTypeDefSpec(obj: any, propertyName: string): string {
const fields = obj?.$type?.fields;
if (!fields) {
return null;
}
const options = fields[propertyName]?.options;
if (!options) {
return null;
}
return options["(.android.typedef)"];
}
/**
* Translate intdef properties into their string representation
*
* For proto objects check the
*
* @param parentObj Object containing the value to parse
* @param propertyName Property to search
* @param value Property value
*/
private static translateIntDef(parentObj: any, propertyName: string, value: any): string {
const parentClassName = parentObj.constructor.name;
const propertyPath = `${parentClassName}.${propertyName}`;
let translatedValue = value;
// Parse Flicker objects (no intdef annotation supported)
if (this.FLICKER_INTDEF_MAP.has(propertyPath)) {
translatedValue = this.getIntFlagsAsStrings(value,
this.FLICKER_INTDEF_MAP.get(propertyPath));
} else {
// If it's a proto, search on the proto definition for the intdef type
const typeDefSpec = this.getTypeDefSpec(parentObj, propertyName);
if (typeDefSpec) {
translatedValue = this.getIntFlagsAsStrings(value, typeDefSpec);
}
}
return translatedValue;
}
/**
* Translate a property from its numerical value into its string representation
*
* @param intFlags Property value
* @param annotationType IntDef type to use
*/
private static getIntFlagsAsStrings(intFlags: any, annotationType: string) {
const flags = [];
const mapping = intDefMapping[annotationType].values;
const knownFlagValues = Object.keys(mapping).reverse().map(x => parseInt(x));
if (mapping.length == 0) {
console.warn("No mapping for type", annotationType)
return intFlags + ""
}
// Will only contain bits that have not been associated with a flag.
const parsedIntFlags = parseInt(intFlags);
let leftOver = parsedIntFlags;
for (const flagValue of knownFlagValues) {
if (((leftOver & flagValue) && ((intFlags & flagValue) === flagValue))
|| (parsedIntFlags === 0 && flagValue === 0)) {
flags.push(mapping[flagValue]);
leftOver = leftOver & ~flagValue;
}
}
if (flags.length === 0) {
console.error('No valid flag mappings found for ',
intFlags, 'of type', annotationType);
}
if (leftOver) {
// If 0 is a valid flag value that isn't in the intDefMapping
// it will be ignored
flags.push(leftOver);
}
return flags.join(' | ');
}
}

View File

@@ -1,12 +0,0 @@
This directory contains all the code extending the common Flicker library
to make it fully compatible with Winscope. The common Flicker library is
written is Kotlin and compiled to JavaScript and then extended by the code in
this directory.
To use flickerlib in the rest of the Winscope source code use
`import { ... } from '@/flickerlib'` rather than importing the compiled
common Flicker library directly.
The flickerlib classes are extended through mixins (functions, getter, and
setters) that are injected into the original compiled common Flicker library
classes.

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2020, 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 { TagTrace } from "./common"
import TagState from "./tags/TagState"
TagTrace.fromProto = function (proto: any): TagTrace {
const states = [];
for (const stateProto of proto.states) {
const transformedState = TagState.fromProto(
stateProto.timestamp,
stateProto.tags);
states.push(transformedState);
}
const source = null;
return new TagTrace(states, source);
}
export default TagTrace;

View File

@@ -1,118 +0,0 @@
/*
* Copyright 2020, 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 {
KeyguardControllerState,
RootWindowContainer,
WindowManagerPolicy,
WindowManagerState,
WINDOW_MANAGER_KIND,
} from "./common"
import WindowContainer from "./windows/WindowContainer"
WindowManagerState.fromProto = function (proto: any, timestamp: number = 0, where: string = ""): WindowManagerState {
var inputMethodWIndowAppToken = "";
if (proto.inputMethodWindow != null) {
proto.inputMethodWindow.hashCode.toString(16)
};
const rootWindowContainer = createRootWindowContainer(proto.rootWindowContainer);
const keyguardControllerState = createKeyguardControllerState(
proto.rootWindowContainer.keyguardController);
const policy = createWindowManagerPolicy(proto.policy);
const entry = new WindowManagerState(
where,
policy,
proto.focusedApp,
proto.focusedDisplayId,
proto.focusedWindow?.title ?? "",
inputMethodWIndowAppToken,
proto.rootWindowContainer.isHomeRecentsComponent,
proto.displayFrozen,
proto.rootWindowContainer.pendingActivities.map(it => it.title),
rootWindowContainer,
keyguardControllerState,
/*timestamp */ `${timestamp}`
);
addAttributes(entry, proto);
return entry
}
function addAttributes(entry: WindowManagerState, proto: any) {
entry.kind = WINDOW_MANAGER_KIND;
// There no JVM/JS translation for Longs yet
entry.timestampMs = entry.timestamp.toString();
entry.rects = entry.windowStates.reverse().map(it => it.rect);
if (!entry.isComplete()) {
entry.isIncompleteReason = entry.getIsIncompleteReason();
}
entry.proto = proto;
entry.shortName = entry.name;
entry.chips = [];
entry.isVisible = true;
}
function createWindowManagerPolicy(proto: any): WindowManagerPolicy {
return new WindowManagerPolicy(
proto.focusedAppToken ?? "",
proto.forceStatusBar,
proto.forceStatusBarFromKeyguard,
proto.keyguardDrawComplete,
proto.keyguardOccluded,
proto.keyguardOccludedChanged,
proto.keyguardOccludedPending,
proto.lastSystemUiFlags,
proto.orientation,
proto.rotation,
proto.rotationMode,
proto.screenOnFully,
proto.windowManagerDrawComplete
);
}
function createRootWindowContainer(proto: any): RootWindowContainer {
const windowContainer = WindowContainer.fromProto(
/* proto */ proto.windowContainer,
/* childrenProto */ proto.windowContainer?.children?.reverse() ?? [],
/* isActivityInTree */ false
);
if (windowContainer == null) {
throw new Error(`Window container should not be null.\n${JSON.stringify(proto)}`);
}
const entry = new RootWindowContainer(windowContainer);
return entry;
}
function createKeyguardControllerState(proto: any): KeyguardControllerState {
const keyguardOccludedStates = {};
if (proto) {
proto.keyguardOccludedStates.forEach(it =>
keyguardOccludedStates[it.displayId] = it.keyguardOccluded);
}
return new KeyguardControllerState(
proto?.isAodShowing ?? false,
proto?.isKeyguardShowing ?? false,
keyguardOccludedStates
);
}
export default WindowManagerState;

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2020, 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 { WindowManagerTrace } from "./common"
import WindowManagerState from "./WindowManagerState"
WindowManagerTrace.fromProto = function (proto: any) {
const entries = [];
for (const entryProto of proto.entry) {
const transformedEntry = WindowManagerState.fromProto(
entryProto.windowManagerService,
entryProto.elapsedRealtimeNanos,
entryProto.where);
entries.push(transformedEntry);
}
const source = null;
return new WindowManagerTrace(entries, source);
}
WindowManagerTrace.fromDump = function(proto: any): WindowManagerTrace {
return WindowManagerState.fromProto(proto);
}
export default WindowManagerTrace;

View File

@@ -1,320 +0,0 @@
/*
* Copyright 2020, 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.
*/
// Imports all the compiled common Flicker library classes and exports them
// as clean es6 modules rather than having them be commonjs modules
// WM
const WindowManagerTrace = require('flicker').com.android.server.wm.traces.
common.windowmanager.WindowManagerTrace;
const WindowManagerState = require('flicker').com.android.server.wm.traces.
common.windowmanager.WindowManagerState;
const Activity = require('flicker').com.android.server.wm.traces.common.
windowmanager.windows.Activity;
const Configuration = require('flicker').com.android.server.wm.traces.common.
windowmanager.windows.Configuration;
const ConfigurationContainer = require('flicker').com.android.server.wm.traces.
common.windowmanager.windows.ConfigurationContainer;
const DisplayArea = require('flicker').com.android.server.wm.traces.common.
windowmanager.windows.DisplayArea;
const DisplayContent = require('flicker').com.android.server.wm.traces.common.
windowmanager.windows.DisplayContent;
const KeyguardControllerState = require('flicker').com.android.server.wm.
traces.common.windowmanager.windows.KeyguardControllerState;
const RootWindowContainer = require('flicker').com.android.server.wm.traces.
common.windowmanager.windows.RootWindowContainer;
const Task = require('flicker').com.android.server.wm.traces.common.
windowmanager.windows.Task;
const TaskFragment = require('flicker').com.android.server.wm.traces.common.
windowmanager.windows.TaskFragment;
const WindowConfiguration = require('flicker').com.android.server.wm.traces.
common.windowmanager.windows.WindowConfiguration;
const WindowContainer = require('flicker').com.android.server.wm.traces.common.
windowmanager.windows.WindowContainer;
const WindowLayoutParams= require('flicker').com.android.server.wm.traces.
common.windowmanager.windows.WindowLayoutParams;
const WindowManagerPolicy = require('flicker').com.android.server.wm.traces.
common.windowmanager.windows.WindowManagerPolicy;
const WindowState = require('flicker').com.android.server.wm.traces.common.
windowmanager.windows.WindowState;
const WindowToken = require('flicker').com.android.server.wm.traces.common.
windowmanager.windows.WindowToken;
const WINDOW_MANAGER_KIND = 'WindowManagerState';
// SF
const Layer = require('flicker').com.android.server.wm.traces.common.
layers.Layer;
const LayerProperties = require('flicker').com.android.server.wm.traces.common.
layers.LayerProperties;
const BaseLayerTraceEntry = require('flicker').com.android.server.wm.traces.common.
layers.BaseLayerTraceEntry;
const LayerTraceEntry = require('flicker').com.android.server.wm.traces.common.
layers.LayerTraceEntry;
const LayerTraceEntryBuilder = require('flicker').com.android.server.wm.traces.
common.layers.LayerTraceEntryBuilder;
const LayersTrace = require('flicker').com.android.server.wm.traces.common.
layers.LayersTrace;
const Matrix22 = require('flicker').com.android.server.wm.traces.common
.Matrix22;
const Matrix33 = require('flicker').com.android.server.wm.traces.common
.Matrix33;
const Transform = require('flicker').com.android.server.wm.traces.common.
layers.Transform;
const Display = require('flicker').com.android.server.wm.traces.common.
layers.Display;
const SURFACE_FLINGER_KIND = 'SurfaceFlingerLayer';
// Common
const Size = require('flicker').com.android.server.wm.traces.common.Size;
const ActiveBuffer = require('flicker').com.android.server.wm.traces.common
.ActiveBuffer;
const Color3 = require('flicker').com.android.server.wm.traces.common.Color3;
const Color = require('flicker').com.android.server.wm.traces.common.Color;
const Point = require('flicker').com.android.server.wm.traces.common.Point;
const Rect = require('flicker').com.android.server.wm.traces.common.Rect;
const RectF = require('flicker').com.android.server.wm.traces.common.RectF;
const Region = require('flicker').com.android.server.wm.traces.common.region.Region;
//Tags
const Tag = require('flicker').com.android.server.wm.traces.common.tags.Tag;
const TagState = require('flicker').com.android.server.wm.traces.common.tags.TagState;
const TagTrace = require('flicker').com.android.server.wm.traces.common.tags.TagTrace;
//Errors
const Error = require('flicker').com.android.server.wm.traces.common.errors.Error;
const ErrorState = require('flicker').com.android.server.wm.traces.common.errors.ErrorState;
const ErrorTrace = require('flicker').com.android.server.wm.traces.common.errors.ErrorTrace;
// Service
const TaggingEngine = require('flicker').com.android.server.wm.traces.common.service.TaggingEngine;
const EMPTY_BUFFER = new ActiveBuffer(0, 0, 0, 0);
const EMPTY_COLOR3 = new Color3(-1, -1, -1);
const EMPTY_COLOR = new Color(-1, -1, -1, 0);
const EMPTY_RECT = new Rect(0, 0, 0, 0);
const EMPTY_RECTF = new RectF(0, 0, 0, 0);
const EMPTY_POINT = new Point(0, 0);
const EMPTY_MATRIX22 = new Matrix22(0, 0, 0, 0, 0, 0);
const EMPTY_MATRIX33 = new Matrix33(0, 0, 0, 0, 0, 0);
const EMPTY_TRANSFORM = new Transform(0, EMPTY_MATRIX33);
function toSize(proto) {
if (proto == null) {
return EMPTY_BOUNDS;
}
const width = proto.width ?? proto.w ?? 0;
const height = proto.height ?? proto.h ?? 0;
if (width || height) {
return new Size(width, height);
}
return EMPTY_BOUNDS;
}
function toActiveBuffer(proto) {
const width = proto?.width ?? 0;
const height = proto?.height ?? 0;
const stride = proto?.stride ?? 0;
const format = proto?.format ?? 0;
if (width || height || stride || format) {
return new ActiveBuffer(width, height, stride, format);
}
return EMPTY_BUFFER;
}
function toColor3(proto) {
if (proto == null) {
return EMPTY_COLOR;
}
const r = proto.r ?? 0;
const g = proto.g ?? 0;
const b = proto.b ?? 0;
if (r || g || b) {
return new Color3(r, g, b);
}
return EMPTY_COLOR3;
}
function toColor(proto) {
if (proto == null) {
return EMPTY_COLOR;
}
const r = proto.r ?? 0;
const g = proto.g ?? 0;
const b = proto.b ?? 0;
const a = proto.a ?? 0;
if (r || g || b || a) {
return new Color(r, g, b, a);
}
return EMPTY_COLOR;
}
function toPoint(proto) {
if (proto == null) {
return null;
}
const x = proto.x ?? 0;
const y = proto.y ?? 0;
if (x || y) {
return new Point(x, y);
}
return EMPTY_POINT;
}
function toRect(proto) {
if (proto == null) {
return EMPTY_RECT;
}
const left = proto?.left ?? 0;
const top = proto?.top ?? 0;
const right = proto?.right ?? 0;
const bottom = proto?.bottom ?? 0;
if (left || top || right || bottom) {
return new Rect(left, top, right, bottom);
}
return EMPTY_RECT;
}
function toRectF(proto) {
if (proto == null) {
return EMPTY_RECTF;
}
const left = proto?.left ?? 0;
const top = proto?.top ?? 0;
const right = proto?.right ?? 0;
const bottom = proto?.bottom ?? 0;
if (left || top || right || bottom) {
return new RectF(left, top, right, bottom);
}
return EMPTY_RECTF;
}
function toRegion(proto) {
if (proto == null) {
return null;
}
const rects = [];
for (let x = 0; x < proto.rect.length; x++) {
const rect = proto.rect[x];
const parsedRect = toRect(rect);
rects.push(parsedRect);
}
return new Region(rects);
}
function toTransform(proto) {
if (proto == null) {
return EMPTY_TRANSFORM;
}
const dsdx = proto.dsdx ?? 0;
const dtdx = proto.dtdx ?? 0;
const tx = proto.tx ?? 0;
const dsdy = proto.dsdy ?? 0;
const dtdy = proto.dtdy ?? 0;
const ty = proto.ty ?? 0;
if (dsdx || dtdx || tx || dsdy || dtdy || ty) {
const matrix = new Matrix33(dsdx, dtdx, tx, dsdy, dtdy, ty);
return new Transform(proto.type ?? 0, matrix);
}
if (proto.type) {
return new Transform(proto.type ?? 0, EMPTY_MATRIX33);
}
return EMPTY_TRANSFORM;
}
function toMatrix22(proto) {
if (proto == null) {
return EMPTY_MATRIX22;
}
const dsdx = proto.dsdx ?? 0;
const dtdx = proto.dtdx ?? 0;
const dsdy = proto.dsdy ?? 0;
const dtdy = proto.dtdy ?? 0;
if (dsdx || dtdx || dsdy || dtdy) {
return new Matrix22(dsdx, dtdx, dsdy, dtdy);
}
return EMPTY_MATRIX22;
}
export {
Activity,
Configuration,
ConfigurationContainer,
DisplayArea,
DisplayContent,
KeyguardControllerState,
RootWindowContainer,
Task,
TaskFragment,
WindowConfiguration,
WindowContainer,
WindowState,
WindowToken,
WindowLayoutParams,
WindowManagerPolicy,
WindowManagerTrace,
WindowManagerState,
WINDOW_MANAGER_KIND,
// SF
BaseLayerTraceEntry,
Layer,
LayerProperties,
LayerTraceEntry,
LayerTraceEntryBuilder,
LayersTrace,
Transform,
Matrix22,
Matrix33,
Display,
SURFACE_FLINGER_KIND,
// Tags
Tag,
TagState,
TagTrace,
// Errors
Error,
ErrorState,
ErrorTrace,
// Common
Size,
ActiveBuffer,
Color,
Color3,
Point,
Rect,
RectF,
Region,
// Service
TaggingEngine,
toSize,
toActiveBuffer,
toColor,
toColor3,
toPoint,
toRect,
toRectF,
toRegion,
toMatrix22,
toTransform,
};

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2021, 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 { Error } from "../common"
Error.fromProto = function (proto: any): Error {
const error = new Error(
proto.stacktrace,
proto.message,
proto.layerId,
proto.windowToken,
proto.taskId,
proto.assertionName
);
return error;
}
export default Error;

View File

@@ -1,26 +0,0 @@
/*
* Copyright 2020, 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 { ErrorState } from "../common";
import Error from './Error';
ErrorState.fromProto = function (protos: any[], timestamp: number): ErrorState {
const errors = protos.map(it => Error.fromProto(it));
const state = new ErrorState(errors, `${timestamp}`);
return state;
}
export default ErrorState;

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2020, 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 LayersTrace from './LayersTrace';
import WindowManagerState from './WindowManagerState';
import WindowManagerTrace from './WindowManagerTrace';
import ObjectFormatter from './ObjectFormatter';
import TagTrace from './TagTrace';
import ErrorTrace from './ErrorTrace';
/**
* Entry point into the flickerlib for Winscope.
* Expose everything we want Winscope to have access to here.
*/
export {ObjectFormatter, LayersTrace, WindowManagerState, WindowManagerTrace, TagTrace, ErrorTrace};

View File

@@ -1,112 +0,0 @@
/*
* Copyright 2021, 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 { Layer, LayerProperties, Rect, toActiveBuffer, toColor, toRect, toRectF, toRegion } from "../common"
import { shortenName } from '../mixin'
import { RELATIVE_Z_CHIP, GPU_CHIP, HWC_CHIP } from '../treeview/Chips'
import Transform from './Transform'
Layer.fromProto = function (proto: any): Layer {
const visibleRegion = toRegion(proto.visibleRegion)
const activeBuffer = toActiveBuffer(proto.activeBuffer)
const bounds = toRectF(proto.bounds)
const color = toColor(proto.color)
const screenBounds = toRectF(proto.screenBounds)
const sourceBounds = toRectF(proto.sourceBounds)
const transform = Transform.fromProto(proto.transform, proto.position)
const bufferTransform = Transform.fromProto(proto.bufferTransform, /* position */ null)
const hwcCrop = toRectF(proto.hwcCrop)
const hwcFrame = toRect(proto.hwcFrame)
const requestedColor = toColor(proto.requestedColor)
const requestedTransform =
Transform.fromProto(proto.requestedTransform, proto.requestedPosition)
const cornerRadiusCrop = toRectF(proto.cornerRadiusCrop)
const inputTransform =
Transform.fromProto(proto.inputWindowInfo ? proto.inputWindowInfo.transform : null)
const inputRegion =
toRegion(proto.inputWindowInfo ? proto.inputWindowInfo.touchableRegion : null)
let crop: Rect
if (proto.crop) {
crop = toRect(proto.crop)
};
const properties = new LayerProperties(
visibleRegion,
activeBuffer,
/* flags */ proto.flags,
bounds,
color,
/* isOpaque */ proto.isOpaque,
/* shadowRadius */ proto.shadowRadius,
/* cornerRadius */ proto.cornerRadius,
/* type */ proto.type ?? ``,
screenBounds,
transform,
sourceBounds,
/* effectiveScalingMode */ proto.effectiveScalingMode,
bufferTransform,
/* hwcCompositionType */ proto.hwcCompositionType,
hwcCrop,
hwcFrame,
/* backgroundBlurRadius */ proto.backgroundBlurRadius,
crop,
/* isRelativeOf */ proto.isRelativeOf,
/* zOrderRelativeOfId */ proto.zOrderRelativeOf,
/* stackId */ proto.layerStack,
requestedTransform,
requestedColor,
cornerRadiusCrop,
inputTransform,
inputRegion
);
const entry = new Layer(
/* name */ proto.name ?? ``,
/* id */ proto.id,
/*parentId */ proto.parent,
/* z */ proto.z,
/* currFrame */ proto.currFrame,
properties
);
addAttributes(entry, proto);
return entry
}
function addAttributes(entry: Layer, proto: any) {
entry.kind = `${entry.id}`;
entry.shortName = shortenName(entry.name);
entry.proto = proto;
entry.rect = entry.bounds;
entry.rect.transform = entry.transform;
entry.rect.ref = entry;
entry.rect.label = entry.name;
entry.chips = [];
updateChips(entry);
}
function updateChips(entry) {
if ((entry.zOrderRelativeOf || -1) !== -1) {
entry.chips.push(RELATIVE_Z_CHIP);
}
if (entry.hwcCompositionType === 'CLIENT') {
entry.chips.push(GPU_CHIP);
} else if (entry.hwcCompositionType === 'DEVICE' || entry.hwcCompositionType === 'SOLID_COLOR') {
entry.chips.push(HWC_CHIP);
}
}
export default Layer;

View File

@@ -1,80 +0,0 @@
/*
* Copyright 2021, 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 { Display, LayerTraceEntry, LayerTraceEntryBuilder, toRect, toSize, toTransform, SURFACE_FLINGER_KIND } from "../common"
import Layer from './Layer'
import { VISIBLE_CHIP, RELATIVE_Z_PARENT_CHIP, MISSING_LAYER } from '../treeview/Chips'
LayerTraceEntry.fromProto = function (protos: any[], displayProtos: any[],
timestamp: number, hwcBlob: string, where: string = ''): LayerTraceEntry {
const layers = protos.map(it => Layer.fromProto(it));
const displays = (displayProtos || []).map(it => newDisplay(it));
const builder = new LayerTraceEntryBuilder(timestamp, layers, displays, hwcBlob, where);
const entry: LayerTraceEntry = builder.build();
updateChildren(entry);
addAttributes(entry, protos);
return entry;
}
function addAttributes(entry: LayerTraceEntry, protos: any) {
entry.kind = SURFACE_FLINGER_KIND;
// There no JVM/JS translation for Longs yet
entry.timestampMs = entry.timestamp.toString()
entry.rects = entry.visibleLayers
.sort((a, b) => (b.absoluteZ > a.absoluteZ) ? 1 : (a.absoluteZ == b.absoluteZ) ? 0 : -1)
.map(it => it.rect);
// Avoid parsing the entry root because it is an array of layers
// containing all trace information, this slows down the property tree.
// Instead parse only key properties for debugging
const entryIds = {}
protos.forEach(it =>
entryIds[it.id] = `\nparent=${it.parent}\ntype=${it.type}\nname=${it.name}`
);
entry.proto = entryIds;
entry.shortName = entry.name;
entry.chips = [];
entry.isVisible = true;
}
function updateChildren(entry: LayerTraceEntry) {
entry.flattenedLayers.forEach(it => {
if (it.isVisible) {
it.chips.push(VISIBLE_CHIP);
}
if (it.zOrderRelativeOf) {
it.chips.push(RELATIVE_Z_PARENT_CHIP);
}
if (it.isMissing) {
it.chips.push(MISSING_LAYER);
}
});
}
function newDisplay(proto: any): Display {
return new Display(
proto.id,
proto.name,
proto.layerStack,
toSize(proto.size),
toRect(proto.layerStackSpaceRect),
toTransform(proto.transform),
proto.isVirtual
)
}
export default LayerTraceEntry;

View File

@@ -1,114 +0,0 @@
/*
* Copyright 2021, 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 { BaseLayerTraceEntry } from "../common";
import LayerTraceEntry from "./LayerTraceEntry";
class LayerTraceEntryLazy extends BaseLayerTraceEntry {
private _isInitialized: boolean = false;
private _layersProto: any[];
private _displayProtos: any[];
timestamp: number;
timestampMs: string;
hwcBlob: string;
where: string;
private _lazyLayerTraceEntry: LayerTraceEntry;
constructor (layersProto: any[], displayProtos: any[],
timestamp: number, hwcBlob: string, where: string = '') {
super();
this._layersProto = layersProto;
this._displayProtos = displayProtos;
this.timestamp = timestamp;
this.timestampMs = timestamp.toString();
this.hwcBlob = hwcBlob;
this.where = where;
this.declareLazyProperties();
}
private initialize() {
if (this._isInitialized) return;
this._isInitialized = true;
this._lazyLayerTraceEntry = LayerTraceEntry.fromProto(
this._layersProto, this._displayProtos, this.timestamp,
this.hwcBlob, this.where);
this._layersProto = [];
this._displayProtos = [];
}
private declareLazyProperties() {
Object.defineProperty(this, 'kind', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.kind;
}});
Object.defineProperty(this, 'timestampMs', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.timestampMs;
}});
Object.defineProperty(this, 'rects', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.rects;
}});
Object.defineProperty(this, 'proto', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.proto;
}});
Object.defineProperty(this, 'shortName', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.shortName;
}});
Object.defineProperty(this, 'isVisible', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.isVisible;
}});
Object.defineProperty(this, 'flattenedLayers', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.flattenedLayers;
}});
Object.defineProperty(this, 'stableId', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.stableId;
}});
Object.defineProperty(this, 'visibleLayers', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.visibleLayers;
}});
Object.defineProperty(this, 'children', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.children;
}});
Object.defineProperty(this, 'displays', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.displays;
}});
}
}
export default LayerTraceEntryLazy;

View File

@@ -1,90 +0,0 @@
/*
* Copyright 2021, 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 { Transform, Matrix33 } from "../common"
Transform.fromProto = function (transformProto, positionProto): Transform {
const entry = new Transform(
transformProto?.type ?? 0,
getMatrix(transformProto, positionProto))
return entry
}
function getMatrix(transform, position): Matrix33 {
const x = position?.x ?? 0
const y = position?.y ?? 0
if (transform == null || isSimpleTransform(transform.type)) {
return getDefaultTransform(transform?.type, x, y)
}
return new Matrix33(transform.dsdx, transform.dtdx, x, transform.dsdy, transform.dtdy, y)
}
function getDefaultTransform(type, x, y): Matrix33 {
// IDENTITY
if (!type) {
return new Matrix33(1, 0, x, 0, 1, y)
}
// ROT_270 = ROT_90|FLIP_H|FLIP_V
if (isFlagSet(type, ROT_90_VAL | FLIP_V_VAL | FLIP_H_VAL)) {
return new Matrix33(0, -1, x, 1, 0, y)
}
// ROT_180 = FLIP_H|FLIP_V
if (isFlagSet(type, FLIP_V_VAL | FLIP_H_VAL)) {
return new Matrix33(-1, 0, x, 0, -1, y)
}
// ROT_90
if (isFlagSet(type, ROT_90_VAL)) {
return new Matrix33(0, 1, x, -1, 0, y)
}
// IDENTITY
if (isFlagClear(type, SCALE_VAL | ROTATE_VAL)) {
return new Matrix33(1, 0, x, 0, 1, y)
}
throw new Error(`Unknown transform type ${type}`)
}
export function isFlagSet(type, bits): Boolean {
var type = type || 0;
return (type & bits) === bits;
}
export function isFlagClear(type, bits): Boolean {
return (type & bits) === 0;
}
export function isSimpleTransform(type): Boolean {
return isFlagClear(type, ROT_INVALID_VAL | SCALE_VAL)
}
/* transform type flags */
const ROTATE_VAL = 0x0002
const SCALE_VAL = 0x0004
/* orientation flags */
const FLIP_H_VAL = 0x0100 // (1 << 0 << 8)
const FLIP_V_VAL = 0x0200 // (1 << 1 << 8)
const ROT_90_VAL = 0x0400 // (1 << 2 << 8)
const ROT_INVALID_VAL = 0x8000 // (0x80 << 8)
export default Transform

View File

@@ -1,61 +0,0 @@
/*
* Copyright 2020, 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 ObjectFormatter from "./ObjectFormatter"
/**
* Get the properties of a WM object for display.
*
* @param entry WM hierarchy element
* @param proto Associated proto object
*/
export function getPropertiesForDisplay(entry: any): any {
if (!entry) {
return
}
let obj: any = {}
const properties = ObjectFormatter.getProperties(entry)
properties.forEach(prop => obj[prop] = entry[prop]);
// we remove the children property from the object to avoid it showing the
// the properties view of the element as we can always see those elements'
// properties by changing the target element in the hierarchy tree view.
if (obj.children) delete obj.children
if (obj.proto) delete obj.proto
obj.proto = Object.assign({}, entry.proto)
if (obj.proto.children) delete obj.proto.children
if (obj.proto.childWindows) delete obj.proto.childWindows
if (obj.proto.childrenWindows) delete obj.proto.childrenWindows
if (obj.proto.childContainers) delete obj.proto.childContainers
if (obj.proto.windowToken) delete obj.proto.windowToken
if (obj.proto.rootDisplayArea) delete obj.proto.rootDisplayArea
if (obj.proto.rootWindowContainer) delete obj.proto.rootWindowContainer
if (obj.proto.windowContainer?.children) delete obj.proto.windowContainer.children
obj = ObjectFormatter.format(obj)
return obj
}
export function shortenName(name: any): string {
const classParts = (name + "").split(".")
if (classParts.length <= 3) {
return name
}
const className = classParts.slice(-1)[0] // last element
return `${classParts[0]}.${classParts[1]}.(...).${className}`
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2021, 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 { Tag } from "../common";
import TransitionType from "./TransitionType";
const transitionTypeMap = new Map([
['ROTATION', TransitionType.ROTATION],
['PIP_ENTER', TransitionType.PIP_ENTER],
['PIP_RESIZE', TransitionType.PIP_RESIZE],
['PIP_EXPAND', TransitionType.PIP_EXPAND],
['PIP_EXIT', TransitionType.PIP_EXIT],
['APP_LAUNCH', TransitionType.APP_LAUNCH],
['APP_CLOSE', TransitionType.APP_CLOSE],
['IME_APPEAR', TransitionType.IME_APPEAR],
['IME_DISAPPEAR', TransitionType.IME_DISAPPEAR],
['APP_PAIRS_ENTER', TransitionType.APP_PAIRS_ENTER],
['APP_PAIRS_EXIT', TransitionType.APP_PAIRS_EXIT],
]);
Tag.fromProto = function (proto: any): Tag {
const tag = new Tag(
proto.id,
transitionTypeMap.get(proto.transition),
proto.isStartTag,
proto.layerId,
proto.windowToken,
proto.taskId
);
return tag;
};
export default Tag;

View File

@@ -1,26 +0,0 @@
/*
* Copyright 2020, 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 { TagState } from "../common";
import Tag from './Tag';
TagState.fromProto = function (timestamp: number, protos: any[]): TagState {
const tags = protos.map(it => Tag.fromProto(it));
const state = new TagState(`${timestamp}`, tags);
return state;
}
export default TagState;

Some files were not shown because too many files have changed in this diff Show More