Remove legacy Winscope
Bug: b/264247967 Test: not needed Change-Id: I61108714c08e0a4ab2e0c5bf25931740218e68e8
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
8
tools/winscope/.gitignore
vendored
8
tools/winscope/.gitignore
vendored
@@ -1,8 +0,0 @@
|
||||
node_modules/
|
||||
adb_proxy/venv/
|
||||
.vscode/
|
||||
dist/
|
||||
kotlin_build/
|
||||
yarn-error.log
|
||||
kotlin_build/
|
||||
.eslintcache
|
||||
@@ -1,5 +0,0 @@
|
||||
natanieljr@google.com
|
||||
pablogamito@google.com
|
||||
keanmariotti@google.com
|
||||
jjaggi@google.com
|
||||
roosa@google.com
|
||||
@@ -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
|
||||
@@ -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")
|
||||
19
tools/winscope/env/dev.env.js
vendored
19
tools/winscope/env/dev.env.js
vendored
@@ -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'
|
||||
};
|
||||
19
tools/winscope/env/prod.env.js
vendored
19
tools/winscope/env/prod.env.js
vendored
@@ -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'
|
||||
};
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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)]);
|
||||
})
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"spec_dir": "spec",
|
||||
"spec_files": [
|
||||
"**/*[sS]pec.js"
|
||||
],
|
||||
"helpers": [
|
||||
"../node_modules/@babel/register/lib/node.js"
|
||||
],
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": false
|
||||
}
|
||||
@@ -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 };
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 };
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 <= 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">, </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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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(' | ');
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user