diff --git a/package-lock.json b/package-lock.json index c752bda..2081631 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,9 @@ "react-scripts": "^4.0.3", "react-table-drag-select": "^0.3.1", "recharts": "^2.1.8", - "timechart": "^1.0.0-beta.4" + "timechart": "^1.0.0-beta.4", + "uplot": "^1.6.18", + "uplot-react": "^1.1.1" }, "devDependencies": { "@craco/craco": "^6.4.3", @@ -21479,6 +21481,23 @@ "yarn": "*" } }, + "node_modules/uplot": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.18.tgz", + "integrity": "sha512-x7+bFfIZ8rMjOmDGhUlJCkYWiZX617xQWNfT94JUhidliRtzMHKIX0xUiN92TZ/7il6xMf9oLwbhsz7nbqF1YQ==" + }, + "node_modules/uplot-react": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/uplot-react/-/uplot-react-1.1.1.tgz", + "integrity": "sha512-zCvwyZVm4nfYDi+KjaK0FppqftGzga/x+u0h2baRWj1vXMB9/hfJ1qb9gXAdXMfp17C9Rk57HoZDE9MewNWLfg==", + "engines": { + "node": ">=8.10" + }, + "peerDependencies": { + "react": ">=16.8.6", + "uplot": "^1.6.7" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -40124,6 +40143,17 @@ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" }, + "uplot": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.18.tgz", + "integrity": "sha512-x7+bFfIZ8rMjOmDGhUlJCkYWiZX617xQWNfT94JUhidliRtzMHKIX0xUiN92TZ/7il6xMf9oLwbhsz7nbqF1YQ==" + }, + "uplot-react": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/uplot-react/-/uplot-react-1.1.1.tgz", + "integrity": "sha512-zCvwyZVm4nfYDi+KjaK0FppqftGzga/x+u0h2baRWj1vXMB9/hfJ1qb9gXAdXMfp17C9Rk57HoZDE9MewNWLfg==", + "requires": {} + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index fdc3bbb..7dd85fc 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,9 @@ "react-scripts": "^4.0.3", "react-table-drag-select": "^0.3.1", "recharts": "^2.1.8", - "timechart": "^1.0.0-beta.4" + "timechart": "^1.0.0-beta.4", + "uplot": "^1.6.18", + "uplot-react": "^1.1.1" }, "devDependencies": { "@craco/craco": "^6.4.3", diff --git a/public/logs/trigger/composite_1.csv b/public/logs/trigger/composite_1.csv index db1536c..d8d6352 100644 --- a/public/logs/trigger/composite_1.csv +++ b/public/logs/trigger/composite_1.csv @@ -123,6 +123,7 @@ Flag,Flag,Flag,Flag,ms,ms,ms,ms 0.0,0.0,0.0,1.0,564200.94,564200.94,35.864,564200.94 0.0,1.0,1.0,1.0,564250.3,564250.3,49.376,564250.3 1.0,1.0,0.0,1.0,564258.0,564258.0,7.728,564258.0 +MARK 000 1.0,0.0,1.0,1.0,564286.5,564286.5,28.484,564286.5 0.0,0.0,0.0,1.0,564294.0,564294.0,7.468,564294.0 1.0,0.0,0.0,1.0,564351.06,564351.06,57.056,564351.06 @@ -382,4 +383,4 @@ Flag,Flag,Flag,Flag,ms,ms,ms,ms 1.0,0.0,0.0,1.0,568452.75,568452.75,57.408,568452.75 0.0,0.0,0.0,1.0,568488.8,568488.8,36.024,568488.8 0.0,1.0,1.0,1.0,568538.44,568538.44,49.624,568538.44 -MARK 000 +MARK 001 diff --git a/public/logs/trigger/composite_1.csv.gz b/public/logs/trigger/composite_1.csv.gz index bcf8144..bc6f516 100644 Binary files a/public/logs/trigger/composite_1.csv.gz and b/public/logs/trigger/composite_1.csv.gz differ diff --git a/public/logs/trigger/3.csv b/public/logs/trigger/tooth_3.csv similarity index 100% rename from public/logs/trigger/3.csv rename to public/logs/trigger/tooth_3.csv diff --git a/public/logs/trigger/tooth_3.csv.gz b/public/logs/trigger/tooth_3.csv.gz new file mode 100644 index 0000000..8bfdde3 Binary files /dev/null and b/public/logs/trigger/tooth_3.csv.gz differ diff --git a/src/components/CanvasHelp.tsx b/src/components/CanvasHelp.tsx new file mode 100644 index 0000000..da12439 --- /dev/null +++ b/src/components/CanvasHelp.tsx @@ -0,0 +1,29 @@ +import { + Popover, + Space, + Typography, +} from 'antd'; +import { QuestionCircleOutlined } from '@ant-design/icons'; + +const { Text, Title } = Typography; + +const CanvasHelp = () => ( +
+ + Navigation + Pinch to zoom + Drag to pan + Ctrl + wheel scroll to zoom X axis + Hold Shift to speed up zoom 5 times + + } + > + + +
+); + +export default CanvasHelp; diff --git a/src/components/Diagnose.tsx b/src/components/Diagnose.tsx index 8c0382a..d96d7ca 100644 --- a/src/components/Diagnose.tsx +++ b/src/components/Diagnose.tsx @@ -13,6 +13,7 @@ import { Steps, Space, Divider, + Typography, } from 'antd'; import { FileTextOutlined, @@ -28,15 +29,23 @@ import { Config, Logs, } from '@speedy-tuner/types'; -import { loadCompositeLogs } from '../utils/api'; +import { + loadCompositeLogs, + loadToothLogs, +} from '../utils/api'; import store from '../store'; import { formatBytes } from '../utils/number'; import CompositeCanvas from './TriggerLog/CompositeCanvas'; -import { isNumber } from '../utils/tune/expression'; +import TriggerLogsParser, { + CompositeLogEntry, + ToothLogEntry, +} from '../utils/logs/TriggerLogsParser'; +import ToothCanvas from './TriggerLog/ToothCanvas'; const { TabPane } = Tabs; const { Content } = Layout; const { Step } = Steps; + const edgeUnknown = 'Unknown'; const mapStateToProps = (state: AppState) => ({ @@ -46,19 +55,6 @@ const mapStateToProps = (state: AppState) => ({ loadedLogs: state.logs, }); -// TODO: extract this to types package -interface CompositeLogEntry { - type: 'trigger' | 'marker'; - primaryLevel: number; - secondaryLevel: number; - trigger: number; - sync: number; - refTime: number; - maxTime: number; - toothTime: number; - time: number; -} - const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLogs: Logs }) => { const { lg } = useBreakpoint(); const { Sider } = Layout; @@ -83,6 +79,7 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa }, }; const [logs, setLogs] = useState(); + const [toothLogs, setToothLogs] = useState(); useEffect(() => { const controller = new AbortController(); @@ -90,79 +87,24 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa const loadData = async () => { try { - const raw = await loadCompositeLogs((percent, total, edge) => { + const compositeRaw = await loadCompositeLogs((percent, total, edge) => { setProgress(percent); setFileSize(formatBytes(total)); setEdgeLocation(edge || edgeUnknown); }, signal); - setFileSize(formatBytes(raw.byteLength)); - - const buff = pako.inflate(new Uint8Array(raw)); - const string = (new TextDecoder()).decode(buff); - const result: CompositeLogEntry[] = []; + const toothRaw = await loadToothLogs(undefined, signal); + setFileSize(formatBytes(compositeRaw.byteLength)); setStep(1); - // TODO: extract this, make a parser class - string.split('\n').forEach((line, index) => { - const trimmed = line.trim(); + const parser = new TriggerLogsParser(); + const resultComposite = parser.parse(pako.inflate(new Uint8Array(compositeRaw))).getCompositeLogs(); + const resultTooth = parser.parse(pako.inflate(new Uint8Array(toothRaw))).getToothLogs(); - // skip comments - if (trimmed.startsWith('#')) { - return; - } + setLogs(resultComposite); + setToothLogs(resultTooth); - // markers - if (trimmed.startsWith('MARK')) { - const previous = result[result.length - 1] || { - primaryLevel: 0, - secondaryLevel: 0, - trigger: 0, - sync: 0, - refTime: 0, - maxTime: 0, - toothTime: 0, - time: 0, - }; - - result.push({ - type: 'marker', - primaryLevel: previous.primaryLevel, - secondaryLevel: previous.secondaryLevel, - trigger: previous.trigger, - sync: previous.sync, - refTime: previous.refTime, - maxTime: previous.maxTime, - toothTime: previous.toothTime, - time: previous.time, - }); - } - - const split = trimmed.split(','); - if (!isNumber(split[0])) { - return; - } - - const time = Number(split[7]); - if (!time) { - return; - } - - result.push({ - type: 'trigger', - primaryLevel: Number(split[0]), - secondaryLevel: Number(split[1]), - trigger: Number(split[2]), - sync: Number(split[3]), - refTime: Number(split[4]), - maxTime: Number(split[5]), - toothTime: Number(split[6]), - time, - }); - }); - - setLogs(result); setStep(2); } catch (error) { setFetchError(error as Error); @@ -191,7 +133,8 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa } key="files"> - composite.csv + tooth.csv + composite.csv @@ -200,13 +143,22 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
- {logs + {toothLogs && logs ? - + ( + <> + + + + ) : @@ -93,14 +92,14 @@ const Curve = ({ value={`${yLabel} (${yUnits})`} position="left" angle={-90} - style={{ fill: mainColor }} + style={{ fill: Colors.TEXT }} /> `${xLabel} : ${value} ${xUnits}`} formatter={(value: number) => [`${value} ${yUnits}`, yLabel]} contentStyle={{ - backgroundColor: tooltipBg, + backgroundColor: Colors.MAIN, border: 0, boxShadow: '0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%)', borderRadius: 5, @@ -111,7 +110,7 @@ const Curve = ({ strokeWidth={3} type="linear" dataKey="y" - stroke="#1e88ea" + stroke={Colors.ACCENT} animationDuration={animationDuration} /> diff --git a/src/components/Log/LogCanvas.tsx b/src/components/Log/LogCanvas.tsx index 1c06c2c..f591785 100644 --- a/src/components/Log/LogCanvas.tsx +++ b/src/components/Log/LogCanvas.tsx @@ -5,31 +5,13 @@ import { useRef, } from 'react'; import { Logs } from '@speedy-tuner/types'; -import { - Popover, - Space, - Typography, - Grid, -} from 'antd'; -import { QuestionCircleOutlined } from '@ant-design/icons'; +import { Grid } from 'antd'; import TimeChart from 'timechart'; import { EventsPlugin } from 'timechart/dist/lib/plugins_extra/events'; import { colorHsl } from '../../utils/number'; import LandscapeNotice from '../Dialog/LandscapeNotice'; +import CanvasHelp from '../CanvasHelp'; -// enum Colors { -// RED = '#f32450', -// CYAN = '#8dd3c7', -// YELLOW = '#ffff00', -// PURPLE = '#bebada', -// GREEN = '#77de3c', -// BLUE = '#2fe3ff', -// GREY = '#334455', -// WHITE = '#fff', -// BG = '#222629', -// } - -const { Text } = Typography; const { useBreakpoint } = Grid; export interface SelectedField { @@ -155,22 +137,7 @@ const LogCanvas = ({ data, width, height, selectedFields }: Props) => { return ( <> -
- - Navigation - Pinch to zoom - Drag to pan - Ctrl + wheel scroll to zoom X axis - Hold Shift to speed up zoom 5 times - - } - > - - -
+
{ const sync: DataPoint[] = []; data.forEach((entry, index) => { - if (entry.type === 'marker') { + if (entry.type === EntryType.MARKER) { markers.push({ x: index, name: '', }); } - if (entry.type === 'trigger') { + if (entry.type === EntryType.TRIGGER) { const prevSecondary = data[index - 1] ? data[index - 1].secondaryLevel : 0; const currentSecondary = (entry.secondaryLevel + 3) * 2; // apply scale @@ -166,22 +131,7 @@ const CompositeCanvas = ({ data, width, height }: Props) => { return ( <> -
- - Navigation - Pinch to zoom - Drag to pan - Ctrl + wheel scroll to zoom X axis - Hold Shift to speed up zoom 5 times - - } - > - - -
+
{ + const { sm } = useBreakpoint(); + const [options, setOptions] = useState(); + const [plotData, setPlotData] = useState(); + + useEffect(() => { + const xData: number[] = []; + const yData: (number | null)[] = []; + + data.forEach((entry: ToothLogEntry, index) => { + if (entry.type === EntryType.TRIGGER) { + yData.push(entry.toothTime); + xData.push(index); + } + }); + + setPlotData([ + xData, + yData, + ]); + + setOptions({ + title: 'Tooth logs', + width, + height, + scales: { + x: { time: false }, + }, + series: [ + { + label: 'Event', + }, + { + label: 'Tooth time', + points: { show: false }, + stroke: Colors.ACCENT, + fill: Colors.ACCENT, + scale: 'toothTime', + value: (self, rawValue) => `${rawValue.toLocaleString()}μs`, + paths: bars!({ size: [0.6, 100] }), + }, + ], + axes: [ + { + stroke: Colors.TEXT, + grid: { stroke: Colors.MAIN_LIGHT }, + }, + { + scale: 'toothTime', + label: '', + stroke: Colors.TEXT, + grid: { stroke: Colors.MAIN_LIGHT }, + }, + ], + cursor: { + drag: { y: false }, + }, + }); + }, [data, width, height, sm]); + + if (!sm) { + return ; + } + + return ( + <> + + + + ); +}; + +export default ToothCanvas; diff --git a/src/utils/api.ts b/src/utils/api.ts index cffd865..eab8bc4 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -90,8 +90,15 @@ export const loadLogs = (onProgress?: onProgressType, signal?: AbortSignal) => export const loadCompositeLogs = (onProgress?: onProgressType, signal?: AbortSignal) => fetchWithProgress( - 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/composite_1.csv.gz', + 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/composite_1_2.csv.gz', // 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/2.csv.gz', onProgress, signal, ).then((response) => response); + +export const loadToothLogs = (onProgress?: onProgressType, signal?: AbortSignal) => + fetchWithProgress( + 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/tooth_3.csv.gz', + onProgress, + signal, + ).then((response) => response); diff --git a/src/utils/colors.ts b/src/utils/colors.ts new file mode 100644 index 0000000..a42b474 --- /dev/null +++ b/src/utils/colors.ts @@ -0,0 +1,17 @@ +// eslint-disable-next-line import/prefer-default-export +export enum Colors { + RED = '#f32450', + CYAN = '#8dd3c7', + YELLOW = '#ffff00', + PURPLE = '#bebada', + GREEN = '#77de3c', + BLUE = '#2fe3ff', + GREY = '#334455', + + // dark theme + ACCENT = '#1e88ea', + TEXT = '#ddd', + MAIN = '#222629', + MAIN_DARK = '#191C1E', + MAIN_LIGHT = '#2E3338', +} diff --git a/src/utils/logs/TriggerLogsParser.ts b/src/utils/logs/TriggerLogsParser.ts new file mode 100644 index 0000000..08377cc --- /dev/null +++ b/src/utils/logs/TriggerLogsParser.ts @@ -0,0 +1,160 @@ +import { isNumber } from '../tune/expression'; + +export enum EntryType { + TRIGGER = 'trigger', + MARKER = 'marker', +} + +export interface CompositeLogEntry { + type: EntryType; + primaryLevel: number; + secondaryLevel: number; + trigger: number; + sync: number; + refTime: number; + maxTime: number; + toothTime: number; + time: number; +} + +export interface ToothLogEntry { + type: EntryType; + toothTime: number; + time: number; +} + +class TriggerLogsParser { + COMMENT_PREFIX = '#'; + + MARKER_PREFIX = 'MARK'; + + isTooth: boolean = false; + + isComposite: boolean = false; + + resultComposite: CompositeLogEntry[] = []; + + resultTooth: ToothLogEntry[] = []; + + parse(buffer: ArrayBuffer): TriggerLogsParser { + const raw = (new TextDecoder()).decode(buffer); + this.parseCompositeLogs(raw); + this.parseToothLogs(raw); + + return this; + } + + getCompositeLogs(): CompositeLogEntry[] { + return this.resultComposite; + } + + getToothLogs(): ToothLogEntry[] { + return this.resultTooth; + } + + private parseToothLogs(raw: string): void { + this.resultTooth = []; + + raw.split('\n').forEach((line) => { + const trimmed = line.trim(); + + if (trimmed.startsWith(this.COMMENT_PREFIX)) { + return; + } + + if (trimmed.startsWith(this.MARKER_PREFIX)) { + const previous = this.resultTooth[this.resultTooth.length - 1] || { + toothTime: 0, + time: 0, + }; + + this.resultTooth.push({ + type: EntryType.MARKER, + toothTime: previous.toothTime, + time: previous.time, + }); + + return; + } + + const split = trimmed.split(','); + if (!isNumber(split[0])) { + return; + } + + const time = Number(split[1]); + if (!time) { + return; + } + + this.resultTooth.push({ + type: EntryType.TRIGGER, + toothTime: Number(split[0]), + time, + }); + }); + } + + private parseCompositeLogs(raw: string): void { + this.resultComposite = []; + + raw.split('\n').forEach((line) => { + const trimmed = line.trim(); + + if (trimmed.startsWith(this.COMMENT_PREFIX)) { + return; + } + + if (trimmed.startsWith(this.MARKER_PREFIX)) { + const previous = this.resultComposite[this.resultComposite.length - 1] || { + primaryLevel: 0, + secondaryLevel: 0, + trigger: 0, + sync: 0, + refTime: 0, + maxTime: 0, + toothTime: 0, + time: 0, + }; + + this.resultComposite.push({ + type: EntryType.MARKER, + primaryLevel: previous.primaryLevel, + secondaryLevel: previous.secondaryLevel, + trigger: previous.trigger, + sync: previous.sync, + refTime: previous.refTime, + maxTime: previous.maxTime, + toothTime: previous.toothTime, + time: previous.time, + }); + + return; + } + + const split = trimmed.split(','); + if (!isNumber(split[0])) { + return; + } + + const time = Number(split[7]); + if (!time) { + return; + } + + this.resultComposite.push({ + type: EntryType.TRIGGER, + primaryLevel: Number(split[0]), + secondaryLevel: Number(split[1]), + trigger: Number(split[2]), + sync: Number(split[3]), + refTime: Number(split[4]), + maxTime: Number(split[5]), + toothTime: Number(split[6]), + time, + }); + }); + } +} + +export default TriggerLogsParser;