Trying to optimize logs JS code

This commit is contained in:
Igor Katson 2023-12-09 12:25:14 +00:00
parent 2603b20f27
commit 98d6a6f054
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
11 changed files with 3100 additions and 2989 deletions

View file

@ -837,10 +837,10 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler {
} }
Message::Have(h) => self.on_have(h), Message::Have(h) => self.on_have(h),
Message::NotInterested => { Message::NotInterested => {
debug!("received \"not interested\", but we don't process it yet") trace!("received \"not interested\", but we don't process it yet")
} }
Message::Cancel(_) => { Message::Cancel(_) => {
debug!("received \"cancel\", but we don't process it yet") trace!("received \"cancel\", but we don't process it yet")
} }
message => { message => {
warn!("received unsupported message {:?}, ignoring", message); warn!("received unsupported message {:?}, ignoring", message);
@ -1306,7 +1306,7 @@ impl PeerHandler {
} }
fn on_i_am_unchoked(&self) { fn on_i_am_unchoked(&self) {
debug!("we are unchoked"); trace!("we are unchoked");
self.locked.write().i_am_choked = false; self.locked.write().i_am_choked = false;
self.unchoke_notify.notify_waiters(); self.unchoke_notify.notify_waiters();
self.requests_sem.add_permits(16); self.requests_sem.add_permits(16);

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,7 @@
"src": "assets/logo.svg" "src": "assets/logo.svg"
}, },
"index.html": { "index.html": {
"file": "assets/index-050cef91.js", "file": "assets/index-b02909de.js",
"isEntry": true, "isEntry": true,
"src": "index.html" "src": "index.html"
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,25 @@
{ {
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build && ./post-build", "build": "vite build && ./post-build",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"react": "^18.2.0", "lodash.debounce": "^4.0.8",
"react-bootstrap": "^2.9.1", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-bootstrap": "^2.9.1",
"react-icons": "^4.12.0" "react-dom": "^18.2.0",
}, "react-icons": "^4.12.0"
"devDependencies": { },
"@types/react": "^18.2.38", "devDependencies": {
"@types/react-dom": "^18.2.16", "@types/lodash.debounce": "^4.0.9",
"prettier": "3.1.0", "@types/react": "^18.2.38",
"typescript": "^5.3.2", "@types/react-dom": "^18.2.16",
"vite": "^4.5.1" "prettier": "3.1.0",
} "typescript": "^5.3.2",
"vite": "^4.5.1"
}
} }

View file

@ -116,6 +116,48 @@ export interface AddTorrentOptions {
preferred_id?: number | null; preferred_id?: number | null;
} }
export type Value = string | number | boolean;
export interface Span {
name: string;
[key: string]: Value;
}
/*
Example log line
const EXAMPLE_LOG_JSON: JSONLogLine = {
timestamp: "2023-12-08T21:48:13.649165Z",
level: "DEBUG",
fields: { message: "successfully port forwarded 192.168.0.112:4225" },
target: "librqbit_upnp",
span: { port: 4225, name: "manage_port" },
spans: [
{ port: 4225, name: "upnp_forward" },
{
location: "http://192.168.0.1:49152/IGDdevicedesc_brlan0.xml",
name: "upnp_endpoint",
},
{ device: "ARRIS TG3492LG", name: "device" },
{ device: "WANDevice:1", name: "device" },
{ device: "WANConnectionDevice:1", name: "device" },
{ url: "/upnp/control/WANIPConnection0", name: "service" },
{ port: 4225, name: "manage_port" },
],
};
*/
export interface JSONLogLine {
level: string;
timestamp: string;
fields: {
message: string;
[key: string]: Value;
};
target: string;
span: Span;
spans: Span[];
}
export interface RqbitAPI { export interface RqbitAPI {
getHttpBaseUrl: () => string | null; getHttpBaseUrl: () => string | null;
listTorrents: () => Promise<ListTorrentsResponse>; listTorrents: () => Promise<ListTorrentsResponse>;

View file

@ -0,0 +1,88 @@
import React from "react";
import { JSONLogLine, Span } from "../api-types";
const SpanFields: React.FC<{ span: Span }> = ({ span }) => {
let fields = Object.entries(span).filter(([name, value]) => name != "name");
if (fields.length == 0) {
return null;
}
return (
<>
{"{"}
{fields
.map(([name, value]) => {
return (
<span key={name}>
{name} = {value}
</span>
);
})
.reduce((prev, curr) => (
<>
{prev}, {curr}
</>
))}
{"}"}
</>
);
};
const LogSpan: React.FC<{ span: Span }> = ({ span }) => (
<>
<span className="fw-bold">{span.name}</span>
<SpanFields span={span} />
</>
);
const Fields: React.FC<{ fields: JSONLogLine["fields"] }> = ({ fields }) => (
<span
className={`m-1 ${
fields.message.match(/error|fail/g) ? "text-danger" : "text-muted"
}`}
>
{fields.message}
{Object.entries(fields)
.filter(([key, value]) => key != "message")
.map(([key, value]) => (
<span className="m-1" key={key}>
<span className="fst-italic fw-bold">{key}</span>={value}
</span>
))}
</span>
);
export const LogLine: React.FC<{ line: JSONLogLine }> = React.memo(
({ line }) => {
const parsed = line;
const classNameByLevel = (level: string) => {
switch (level) {
case "DEBUG":
return "text-primary";
case "INFO":
return "text-success";
case "WARN":
return "text-warning";
case "ERROR":
return "text-danger";
default:
return "text-muted";
}
};
return (
<p className="font-monospace m-0 text-break" style={{ fontSize: "10px" }}>
<span className="m-1">{parsed.timestamp}</span>
<span className={`m-1 ${classNameByLevel(parsed.level)}`}>
{parsed.level}
</span>
<span className="m-1">
{parsed.spans?.map((span, i) => <LogSpan key={i} span={span} />)}
</span>
<span className="m-1 text-muted">{parsed.target}</span>
<Fields fields={parsed.fields} />
</p>
);
}
);

View file

@ -8,13 +8,17 @@ import React, {
import { ErrorWithLabel } from "../rqbit-web"; import { ErrorWithLabel } from "../rqbit-web";
import { ErrorComponent } from "./ErrorComponent"; import { ErrorComponent } from "./ErrorComponent";
import { Form } from "react-bootstrap"; import { Form } from "react-bootstrap";
import { loopUntilSuccess } from "../helper/loopUntilSuccess";
import debounce from "lodash.debounce";
import { LogLine } from "./LogLine";
import { JSONLogLine } from "../api-types";
interface LogStreamProps { interface LogStreamProps {
httpApiBase: string; httpApiBase: string;
maxLines?: number; maxLines?: number;
} }
interface Line { export interface Line {
id: number; id: number;
content: string; content: string;
parsed: JSONLogLine; parsed: JSONLogLine;
@ -22,6 +26,12 @@ interface Line {
} }
const mergeBuffers = (a1: Uint8Array, a2: Uint8Array): Uint8Array => { const mergeBuffers = (a1: Uint8Array, a2: Uint8Array): Uint8Array => {
if (a1.length === 0) {
return a2;
}
if (a2.length === 0) {
return a1;
}
const merged = new Uint8Array(a1.length + a2.length); const merged = new Uint8Array(a1.length + a2.length);
merged.set(a1); merged.set(a1);
merged.set(a2, a1.length); merged.set(a2, a1.length);
@ -30,7 +40,7 @@ const mergeBuffers = (a1: Uint8Array, a2: Uint8Array): Uint8Array => {
const streamLogs = ( const streamLogs = (
httpApiBase: string, httpApiBase: string,
addLine: React.MutableRefObject<(text: string) => void>, addLine: (text: string) => void,
setError: (error: ErrorWithLabel | null) => void setError: (error: ErrorWithLabel | null) => void
): (() => void) => { ): (() => void) => {
const controller = new AbortController(); const controller = new AbortController();
@ -38,28 +48,14 @@ const streamLogs = (
let canceled = false; let canceled = false;
const cancel = () => { const cancelFetch = () => {
console.log("cancelling fetch"); console.log("cancelling fetch");
canceled = true; canceled = true;
controller.abort(); controller.abort();
}; };
const run = async () => { const runOnce = async () => {
let response = null; let response = await fetch(httpApiBase + "/stream_logs", { signal });
try {
response = await fetch(httpApiBase + "/stream_logs", { signal });
} catch (e: any) {
if (canceled) {
return;
}
setError({
text: "network error fetching logs",
details: {
text: e.toString(),
},
});
return null;
}
if (!response.ok) { if (!response.ok) {
let text = await response.text(); let text = await response.text();
@ -70,15 +66,18 @@ const streamLogs = (
text, text,
}, },
}); });
throw new Error("retry");
} }
if (!response.body) { if (!response.body) {
setError({ setError({
text: "error fetching logs: ReadableStream not supported.", text: "error fetching logs: ReadableStream not supported.",
}); });
throw new Error("ReadableStream not supported."); return;
} }
setError(null);
const reader = response.body.getReader(); const reader = response.body.getReader();
let buffer = new Uint8Array(); let buffer = new Uint8Array();
@ -86,150 +85,47 @@ const streamLogs = (
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) { if (done) {
// Handle stream completion or errors setError({
break; text: "log stream terminated",
});
throw new Error("retry");
} }
buffer = mergeBuffers(buffer, value); buffer = mergeBuffers(buffer, value);
while (true) { for (let newLineIdx: number; (newLineIdx = buffer.indexOf(10)) !== -1; ) {
const newLineIdx = buffer.indexOf(10);
if (newLineIdx === -1) {
break;
}
let lineBytes = buffer.slice(0, newLineIdx); let lineBytes = buffer.slice(0, newLineIdx);
let line = new TextDecoder().decode(lineBytes); let line = new TextDecoder().decode(lineBytes);
addLine.current(line); addLine(line);
buffer = buffer.slice(newLineIdx + 1); buffer = buffer.slice(newLineIdx + 1);
} }
} }
}; };
run();
return cancel; let cancelLoop = loopUntilSuccess(
}; () =>
runOnce().then(
type Value = string | number | boolean; () => {},
(e) => {
interface Span { if (canceled) {
name: string; return;
[key: string]: Value; }
} setError({
text: "error streaming logs",
interface JSONLogLine { details: {
level: string; text: e.toString(),
timestamp: string; },
fields: { });
message: string; throw e;
[key: string]: Value; }
}; ),
target: string; 1000
span: Span;
spans: Span[];
}
const EXAMPLE_LOG_JSON: JSONLogLine = {
timestamp: "2023-12-08T21:48:13.649165Z",
level: "DEBUG",
fields: { message: "successfully port forwarded 192.168.0.112:4225" },
target: "librqbit_upnp",
span: { port: 4225, name: "manage_port" },
spans: [
{ port: 4225, name: "upnp_forward" },
{
location: "http://192.168.0.1:49152/IGDdevicedesc_brlan0.xml",
name: "upnp_endpoint",
},
{ device: "ARRIS TG3492LG", name: "device" },
{ device: "WANDevice:1", name: "device" },
{ device: "WANConnectionDevice:1", name: "device" },
{ url: "/upnp/control/WANIPConnection0", name: "service" },
{ port: 4225, name: "manage_port" },
],
};
const LogLine = ({ line }: { line: Line }) => {
const parsed = line.parsed;
const classNameByLevel = (level: string) => {
switch (level) {
case "DEBUG":
return "text-primary";
case "INFO":
return "text-success";
case "WARN":
return "text-warning";
case "ERROR":
return "text-danger";
default:
return "text-muted";
}
};
const spanFields = (span: Span) => {
let fields = Object.entries(span).filter(([name, value]) => name != "name");
if (fields.length == 0) {
return null;
}
return (
<>
{"{"}
{fields
.map(([name, value]) => {
return (
<span key={name}>
{name} = {value}
</span>
);
})
.reduce((prev, curr) => (
<>
{prev}, {curr}
</>
))}
{"}"}
</>
);
};
return (
<p
hidden={!line.show}
className="font-monospace m-0 text-break"
style={{ fontSize: "10px" }}
>
<span className="m-1">{parsed.timestamp}</span>
<span className={`m-1 ${classNameByLevel(parsed.level)}`}>
{parsed.level}
</span>
<span className="m-1">
{parsed.spans?.map((span, i) => (
<span key={i}>
<span className="fw-bold">{span.name}</span>
{spanFields(span)}:
</span>
))}
</span>
<span className="m-1 text-muted">{parsed.target}</span>
<span
className={`m-1 ${
parsed.fields.message.match(/error|fail/g)
? "text-danger"
: "text-muted"
}`}
>
{parsed.fields.message}
{Object.entries(parsed.fields)
.filter(([key, value]) => key != "message")
.map(([key, value]) => (
<span className="m-1" key={key}>
<span className="fst-italic fw-bold">{key}</span>={value}
</span>
))}
</span>
</p>
); );
return () => {
cancelFetch();
cancelLoop();
};
}; };
export const LogStream: React.FC<LogStreamProps> = ({ export const LogStream: React.FC<LogStreamProps> = ({
@ -239,7 +135,7 @@ export const LogStream: React.FC<LogStreamProps> = ({
const [logLines, setLogLines] = useState<Line[]>([]); const [logLines, setLogLines] = useState<Line[]>([]);
const [error, setError] = useState<ErrorWithLabel | null>(null); const [error, setError] = useState<ErrorWithLabel | null>(null);
const [filter, setFilter] = useState<string>(""); const [filter, setFilter] = useState<string>("");
const filterRegex = useRef(new RegExp("")); const filterRegex = useRef<RegExp | null>(null);
const maxL = maxLines ?? 1000; const maxL = maxLines ?? 1000;
@ -253,7 +149,9 @@ export const LogStream: React.FC<LogStreamProps> = ({
id: nextLineId, id: nextLineId,
content: text, content: text,
parsed: JSON.parse(text) as JSONLogLine, parsed: JSON.parse(text) as JSONLogLine,
show: !!text.match(filterRegex.current), show: filterRegex.current
? !!text.match(filterRegex.current)
: true,
}, },
...logLines.slice(0, maxL - 1), ...logLines.slice(0, maxL - 1),
]; ];
@ -266,27 +164,40 @@ export const LogStream: React.FC<LogStreamProps> = ({
const addLineRef = useRef(addLine); const addLineRef = useRef(addLine);
addLineRef.current = addLine; addLineRef.current = addLine;
const updateFilter = debounce((value: string) => {
let regex: RegExp | null = null;
try {
regex = new RegExp(value);
} catch (e) {
return;
}
filterRegex.current = regex;
setLogLines((logLines) => {
let tmp = [...logLines];
for (let line of tmp) {
line.show = !!line.content.match(regex as RegExp);
}
return tmp;
});
}, 200);
const handleFilterChange = (value: string) => { const handleFilterChange = (value: string) => {
setFilter(value); setFilter(value);
try { updateFilter(value);
let regex = new RegExp(value);
filterRegex.current = regex;
setLogLines((logLines) => {
let tmp = [...logLines];
for (let line of tmp) {
line.show = !!line.content.match(regex);
}
return tmp;
});
} catch (e) {}
}; };
useEffect(() => updateFilter.cancel, []);
useEffect(() => { useEffect(() => {
return streamLogs(httpApiBase, addLineRef, setError); return streamLogs(
httpApiBase,
(line) => addLineRef.current(line),
setError
);
}, [httpApiBase]); }, [httpApiBase]);
return ( return (
<div className="row"> <div>
<ErrorComponent error={error} /> <ErrorComponent error={error} />
<div className="mb-3"> <div className="mb-3">
Showing last {maxL} logs since this window was opened Showing last {maxL} logs since this window was opened
@ -296,6 +207,7 @@ export const LogStream: React.FC<LogStreamProps> = ({
<Form.Control <Form.Control
type="text" type="text"
value={filter} value={filter}
name="filter"
placeholder="Enter filter (regex)" placeholder="Enter filter (regex)"
onChange={(e) => handleFilterChange(e.target.value)} onChange={(e) => handleFilterChange(e.target.value)}
/> />
@ -303,7 +215,9 @@ export const LogStream: React.FC<LogStreamProps> = ({
</Form> </Form>
{logLines.map((line) => ( {logLines.map((line) => (
<LogLine key={line.id} line={line} /> <div hidden={!line.show}>
<LogLine key={line.id} line={line.parsed} />
</div>
))} ))}
</div> </div>
); );

View file

@ -6,9 +6,9 @@
"packages": { "packages": {
"": { "": {
"name": "rqbit", "name": "rqbit",
"version": "4.0.0-beta.3",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.5.1", "@tauri-apps/api": "^1.5.1",
"lodash.debounce": "^4.0.8",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.9.1", "react-bootstrap": "^2.9.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -16,6 +16,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": ">=2.0.0-alpha.16", "@tauri-apps/cli": ">=2.0.0-alpha.16",
"@types/lodash.debounce": "^4.0.9",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3", "@vitejs/plugin-react": "^4.0.3",
@ -1087,6 +1088,21 @@
"@babel/types": "^7.20.7" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/lodash": {
"version": "4.14.202",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
"dev": true
},
"node_modules/@types/lodash.debounce": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
"integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.11", "version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
@ -1430,6 +1446,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",

View file

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.5.1", "@tauri-apps/api": "^1.5.1",
"lodash.debounce": "^4.0.8",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.9.1", "react-bootstrap": "^2.9.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -17,6 +18,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": ">=2.0.0-alpha.16", "@tauri-apps/cli": ">=2.0.0-alpha.16",
"@types/lodash.debounce": "^4.0.9",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3", "@vitejs/plugin-react": "^4.0.3",