feat(log): format log entry like native binary

This commit is contained in:
Simon Chan 2022-07-14 13:53:25 +08:00
parent 2621f83ccb
commit db079b4292
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
6 changed files with 157 additions and 27 deletions

View file

@ -175,7 +175,7 @@ export const Grid = withDisplayName('Grid')(({
if (scrollTop < bodyRef.scrollHeight - bodyRef.clientHeight && bodyRef.scrollTop < scrollTop) { if (scrollTop < bodyRef.scrollHeight - bodyRef.clientHeight && bodyRef.scrollTop < scrollTop) {
setAutoScroll(false); setAutoScroll(false);
} }
} else if (bodyRef.scrollTop + bodyRef.offsetHeight >= bodyRef.scrollHeight - 50) { } else if (bodyRef.scrollTop + bodyRef.offsetHeight >= bodyRef.scrollHeight - 10) {
setAutoScroll(true); setAutoScroll(true);
} }

View file

@ -1,11 +1,13 @@
import { ICommandBarItemProps, Stack, StackItem } from "@fluentui/react"; import { Checkbox, ICommandBarItemProps, Stack, StackItem } from "@fluentui/react";
import { makeStyles, mergeClasses, shorthands } from "@griffel/react"; import { makeStyles, mergeClasses, shorthands } from "@griffel/react";
import { AndroidLogEntry, AndroidLogPriority, Logcat } from '@yume-chan/android-bin'; import { AndroidLogEntry, AndroidLogPriority, formatAndroidLogEntry, Logcat, LogcatFormat } from '@yume-chan/android-bin';
import { BTree } from '@yume-chan/b-tree';
import { AbortController, ReadableStream, WritableStream } from '@yume-chan/stream-extra'; import { AbortController, ReadableStream, WritableStream } from '@yume-chan/stream-extra';
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx"; import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { NextPage } from "next"; import { NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import { FormEvent, useEffect, useState } from 'react';
import { CommandBar, Grid, GridColumn, GridHeaderProps, GridRowProps } from "../components"; import { CommandBar, Grid, GridColumn, GridHeaderProps, GridRowProps } from "../components";
import { GlobalState } from "../state"; import { GlobalState } from "../state";
@ -55,6 +57,7 @@ const state = makeAutoObservable({
buffer: [] as LogRow[], buffer: [] as LogRow[],
flushRequested: false, flushRequested: false,
list: [] as LogRow[], list: [] as LogRow[],
selection: new BTree(6),
count: 0, count: 0,
stream: undefined as ReadableStream<AndroidLogEntry> | undefined, stream: undefined as ReadableStream<AndroidLogEntry> | undefined,
stopSignal: undefined as AbortController | undefined, stopSignal: undefined as AbortController | undefined,
@ -98,6 +101,7 @@ const state = makeAutoObservable({
}, },
clear() { clear() {
this.list = []; this.list = [];
this.selection.clear();
this.selectedCount = 0; this.selectedCount = 0;
}, },
get empty() { get empty() {
@ -130,7 +134,16 @@ const state = makeAutoObservable({
disabled: this.selectedCount === 0, disabled: this.selectedCount === 0,
iconProps: { iconName: Icons.Copy }, iconProps: { iconName: Icons.Copy },
onClick: () => { onClick: () => {
let text = '';
for (const index of this.selection) {
text += formatAndroidLogEntry(
this.list[index],
LogcatFormat.Brief
) + '\n';
}
// Chrome on Windows can't copy null characters
text = text.replace(/\u0000/g, '');
navigator.clipboard.writeText(text);
} }
}, },
{ {
@ -139,13 +152,52 @@ const state = makeAutoObservable({
disabled: this.selectedCount === 0, disabled: this.selectedCount === 0,
iconProps: { iconName: Icons.Copy }, iconProps: { iconName: Icons.Copy },
onClick: () => { onClick: () => {
let text = '';
for (const index of this.selection) {
text += this.list[index].message + '\n';
}
// Chrome on Windows can't copy null characters
text = text.replace(/\u0000/g, '');
navigator.clipboard.writeText(text);
} }
} }
]; ];
}, },
get columns(): Column[] { get columns(): Column[] {
return [ return [
{
width: 40,
title: '',
CellComponent: ({ rowIndex, columnIndex, className, ...rest }) => {
const [checked, setChecked] = useState(false);
useEffect(() => {
setChecked(this.selection.has(rowIndex));
}, [rowIndex]);
const handleChange = useStableCallback((e?: FormEvent<EventTarget>, checked?: boolean) => {
if (checked === undefined) {
return;
}
if (checked) {
this.selection.add(rowIndex);
setChecked(true);
} else {
this.selection.delete(rowIndex);
setChecked(false);
}
runInAction(() => {
// Trigger mobx
this.selectedCount = this.selection.size;
});
});
return (
<Stack className={className} verticalAlign='center' horizontalAlign='center' {...rest}>
<Checkbox checked={checked} onChange={handleChange} />
</Stack>
);
}
},
{ {
width: 200, width: 200,
title: 'Time', title: 'Time',

View file

@ -50,6 +50,7 @@ export function register() {
WindowConsole: <WindowConsoleRegular style={STYLE} />, WindowConsole: <WindowConsoleRegular style={STYLE} />,
// Required by @fluentui/react // Required by @fluentui/react
Checkmark: <CheckmarkRegular style={STYLE} />,
StatusCircleCheckmark: <CheckmarkRegular style={STYLE} />, StatusCircleCheckmark: <CheckmarkRegular style={STYLE} />,
ChevronUpSmall: <ChevronUpRegular style={STYLE} />, ChevronUpSmall: <ChevronUpRegular style={STYLE} />,
ChevronDownSmall: <ChevronDownRegular style={STYLE} />, ChevronDownSmall: <ChevronDownRegular style={STYLE} />,

View file

@ -25,6 +25,7 @@ export enum AndroidLogPriority {
Unknown, Unknown,
Default, Default,
Verbose, Verbose,
Debug,
Info, Info,
Warn, Warn,
Error, Error,
@ -32,6 +33,41 @@ export enum AndroidLogPriority {
Silent, Silent,
} }
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=140;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
export const AndroidLogPriorityToCharacter: Record<AndroidLogPriority, string> = {
[AndroidLogPriority.Unknown]: '?',
[AndroidLogPriority.Default]: '?',
[AndroidLogPriority.Verbose]: 'V',
[AndroidLogPriority.Debug]: 'D',
[AndroidLogPriority.Info]: 'I',
[AndroidLogPriority.Warn]: 'W',
[AndroidLogPriority.Error]: 'E',
[AndroidLogPriority.Fatal]: 'F',
[AndroidLogPriority.Silent]: 'S',
};
export enum LogcatFormat {
Brief,
Process,
Tag,
Thread,
Raw,
Time,
ThreadTime,
Long
}
export interface LogcatFormatModifiers {
usec?: boolean;
printable?: boolean;
year?: boolean;
zone?: boolean;
epoch?: boolean;
monotonic?: boolean;
uid?: boolean;
descriptive?: boolean;
}
export interface LogcatOptions { export interface LogcatOptions {
pid?: number; pid?: number;
ids?: LogId[]; ids?: LogId[];
@ -65,6 +101,21 @@ export interface AndroidLogEntry extends LoggerEntry {
message: string; message: string;
} }
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=1415;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
export function formatAndroidLogEntry(
entry: AndroidLogEntry,
format: LogcatFormat = LogcatFormat.Brief,
modifier?: LogcatFormatModifiers
) {
const uid = modifier?.uid ? `${entry.uid.toString().padStart(5)}:` : '';
switch (format) {
// TODO: implement other formats
default:
return `${AndroidLogPriorityToCharacter[entry.priority]}/${entry.tag.padEnd(8)}(${uid}${entry.pid.toString().padStart(5)}): ${entry.message}`;
}
}
function findTagEnd(payload: Uint8Array) { function findTagEnd(payload: Uint8Array) {
for (const separator of [0, ' '.charCodeAt(0), ':'.charCodeAt(0)]) { for (const separator of [0, ' '.charCodeAt(0), ':'.charCodeAt(0)]) {
const index = payload.indexOf(separator); const index = payload.indexOf(separator);
@ -200,6 +251,6 @@ export class Logcat extends AdbCommandBase {
return stdout; return stdout;
}).pipeThrough(new BufferedTransformStream(stream => { }).pipeThrough(new BufferedTransformStream(stream => {
return deserializeAndroidLogEntry(stream); return deserializeAndroidLogEntry(stream);
})) }));
} }
} }

View file

@ -55,13 +55,13 @@ describe('BTree', () => {
const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2); const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2);
for (let value of values) { for (let value of values) {
tree.insert(value); tree.add(value);
validateTree(tree); validateTree(tree);
expect(tree.has(value)).toBe(true); expect(tree.has(value)).toBe(true);
} }
for (let value of values) { for (let value of values) {
tree.remove(value); tree.delete(value);
validateTree(tree); validateTree(tree);
expect(tree.has(value)).toBe(false); expect(tree.has(value)).toBe(false);
} }
@ -73,14 +73,14 @@ describe('BTree', () => {
const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2); const values = Array.from({ length: LENGTH }, (_, i) => i - LENGTH / 2);
shuffle(values); shuffle(values);
for (const value of values) { for (const value of values) {
tree.insert(value); tree.add(value);
validateTree(tree); validateTree(tree);
expect(tree.has(value)).toBe(true); expect(tree.has(value)).toBe(true);
} }
shuffle(values); shuffle(values);
for (const value of values) { for (const value of values) {
tree.remove(value); tree.delete(value);
validateTree(tree); validateTree(tree);
expect(tree.has(value)).toBe(false); expect(tree.has(value)).toBe(false);
} }

View file

@ -137,7 +137,7 @@ export class BTreeNode {
return false; return false;
} }
public insert(value: number): BTreeInsertionResult | boolean { public add(value: number): BTreeInsertionResult | boolean {
let index = this.search(value); let index = this.search(value);
if (index >= 0) { if (index >= 0) {
return false; return false;
@ -155,7 +155,7 @@ export class BTreeNode {
return true; return true;
} }
const split = this.children[index]!.insert(value); const split = this.children[index]!.add(value);
if (typeof split === 'object') { if (typeof split === 'object') {
if (this.keyCount === this.order - 1) { if (this.keyCount === this.order - 1) {
return this.split(split.key, index, split.child); return this.split(split.key, index, split.child);
@ -170,20 +170,20 @@ export class BTreeNode {
return true; return true;
} }
public remove(value: number): boolean { public delete(value: number): boolean {
let index = this.search(value); let index = this.search(value);
if (index >= 0) { if (index >= 0) {
this.removeAt(index); this.deleteAt(index);
return true; return true;
} }
if (this.height > 0) { if (this.height > 0) {
index = ~index; index = ~index;
const removed = this.children[index]!.remove(value); const deleted = this.children[index]!.delete(value);
if (removed) { if (deleted) {
this.balance(index); this.balance(index);
} }
return removed; return deleted;
} }
return false; return false;
@ -265,18 +265,18 @@ export class BTreeNode {
this.keyCount -= 1; this.keyCount -= 1;
} }
protected removeMax(): void { protected deleteMax(): void {
if (this.height === 0) { if (this.height === 0) {
this.keyCount -= 1; this.keyCount -= 1;
return; return;
} }
const child = this.children[this.keyCount]!; const child = this.children[this.keyCount]!;
child.removeMax(); child.deleteMax();
this.balance(this.keyCount); this.balance(this.keyCount);
} }
protected removeAt(index: number) { protected deleteAt(index: number) {
if (this.height === 0) { if (this.height === 0) {
remove(this.keys, this.keyCount, index); remove(this.keys, this.keyCount, index);
this.keyCount -= 1; this.keyCount -= 1;
@ -285,9 +285,23 @@ export class BTreeNode {
const max = this.children[index]!.max(); const max = this.children[index]!.max();
this.keys[index] = max; this.keys[index] = max;
this.children[index]!.removeMax(); this.children[index]!.deleteMax();
this.balance(index); this.balance(index);
} }
public *[Symbol.iterator](): Generator<number, void, void> {
if (this.height > 0) {
for (let i = 0; i < this.keyCount; i += 1) {
yield* this.children[i]!;
yield this.keys[i]!;
}
yield* this.children[this.keyCount]!;
} else {
for (let i = 0; i < this.keyCount; i += 1) {
yield this.keys[i]!;
}
}
}
} }
export class BTree { export class BTree {
@ -323,8 +337,8 @@ export class BTree {
} }
} }
public insert(value: number) { public add(value: number) {
const split = this.root.insert(value); const split = this.root.add(value);
if (typeof split === 'object') { if (typeof split === 'object') {
const keys = new Int32Array(this.order - 1); const keys = new Int32Array(this.order - 1);
keys[0] = split.key; keys[0] = split.key;
@ -347,14 +361,26 @@ export class BTree {
return !!split; return !!split;
} }
public remove(value: number) { public delete(value: number) {
const removed = this.root.remove(value); const deleted = this.root.delete(value);
if (removed) { if (deleted) {
if (this.root.height > 0 && this.root.keyCount === 0) { if (this.root.height > 0 && this.root.keyCount === 0) {
this.root = this.root.children[0]!; this.root = this.root.children[0]!;
} }
this.size -= 1; this.size -= 1;
} }
return removed; return deleted;
}
public clear() {
this.root.keyCount = 0;
this.root.height = 0;
// immediately release all references
this.root.children = new Array(this.order);
this.size = 0;
}
public [Symbol.iterator]() {
return this.root[Symbol.iterator]();
} }
} }