From 2a3579e447459aaeb7998cf968f73c6ef7ff7a0b Mon Sep 17 00:00:00 2001 From: rookisbusy Date: Fri, 7 Apr 2023 20:43:19 +0800 Subject: feat: add memory chat --- src/api/memory.ts | 119 +++++++++++++++++++++++++++++++++++++++++ src/components/Home.tsx | 2 + src/components/MemoryChart.tsx | 61 +++++++++++++++++++++ src/hooks/useLineChart.ts | 20 +++++++ src/i18n/zh.ts | 1 + src/misc/chart-memory.ts | 64 ++++++++++++++++++++++ src/misc/chart.ts | 1 + 7 files changed, 268 insertions(+) create mode 100644 src/api/memory.ts create mode 100644 src/components/MemoryChart.tsx create mode 100644 src/misc/chart-memory.ts (limited to 'src') diff --git a/src/api/memory.ts b/src/api/memory.ts new file mode 100644 index 0000000..b6614bb --- /dev/null +++ b/src/api/memory.ts @@ -0,0 +1,119 @@ +import { ClashAPIConfig } from '~/types'; + +import { buildWebSocketURL, getURLAndInit } from '../misc/request-helper'; + +const endpoint = '/memory'; +const textDecoder = new TextDecoder('utf-8'); + +const Size = 150; + +const memory = { + labels: Array(Size).fill(0), + inuse: Array(Size), + oslimit: Array(Size), + + size: Size, + subscribers: [], + appendData(o: { inuse: number; oslimit: number }) { + this.inuse.shift(); + this.oslimit.shift(); + this.labels.shift(); + + const l = Date.now(); + this.inuse.push(o.inuse); + this.oslimit.push(o.oslimit); + this.labels.push(l); + + this.subscribers.forEach((f) => f(o)); + }, + + subscribe(listener: (x: any) => void) { + this.subscribers.push(listener); + return () => { + const idx = this.subscribers.indexOf(listener); + this.subscribers.splice(idx, 1); + }; + }, +}; + +let fetched = false; +let decoded = ''; + +function parseAndAppend(x: string) { + memory.appendData(JSON.parse(x)); +} + +function pump(reader: ReadableStreamDefaultReader) { + return reader.read().then(({ done, value }) => { + const str = textDecoder.decode(value, { stream: !done }); + decoded += str; + + const splits = decoded.split('\n'); + + const lastSplit = splits[splits.length - 1]; + + for (let i = 0; i < splits.length - 1; i++) { + parseAndAppend(splits[i]); + } + + if (done) { + parseAndAppend(lastSplit); + decoded = ''; + + // eslint-disable-next-line no-console + console.log('GET /memory streaming done'); + fetched = false; + return; + } else { + decoded = lastSplit; + } + return pump(reader); + }); +} + +// 1 OPEN +// other value CLOSED +// similar to ws readyState but not the same +// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState +let wsState: number; +function fetchData(apiConfig: ClashAPIConfig) { + if (fetched || wsState === 1) return memory; + wsState = 1; + const url = buildWebSocketURL(apiConfig, endpoint); + const ws = new WebSocket(url); + ws.addEventListener('error', function (_ev) { + wsState = 3; + }); + ws.addEventListener('close', function (_ev) { + wsState = 3; + fetchDataWithFetch(apiConfig); + }); + ws.addEventListener('message', function (event) { + parseAndAppend(event.data); + }); + return memory; +} + +function fetchDataWithFetch(apiConfig: ClashAPIConfig) { + if (fetched) return memory; + fetched = true; + const { url, init } = getURLAndInit(apiConfig); + fetch(url + endpoint, init).then( + (response) => { + if (response.ok) { + const reader = response.body.getReader(); + pump(reader); + } else { + fetched = false; + } + }, + (err) => { + // eslint-disable-next-line no-console + console.log('fetch /memory error', err); + fetched = false; + } + ); + return memory; +} + +export { fetchData }; diff --git a/src/components/Home.tsx b/src/components/Home.tsx index d7ddbab..6687fbb 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import ContentHeader from './ContentHeader'; import s0 from './Home.module.scss'; import Loading from './Loading'; +import MemoryChart from './MemoryChart'; import TrafficChart from './TrafficChart'; import TrafficNow from './TrafficNow'; @@ -19,6 +20,7 @@ export default function Home() {
}> +
diff --git a/src/components/MemoryChart.tsx b/src/components/MemoryChart.tsx new file mode 100644 index 0000000..9f6febb --- /dev/null +++ b/src/components/MemoryChart.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { State } from '~/store/types'; + +import { fetchData } from '../api/memory'; +import { useLineChartMemory } from '../hooks/useLineChart'; +import { + chartJSResource, + chartStyles, + commonDataSetProps, + memoryChartOptions, +} from '../misc/chart-memory'; +import { getClashAPIConfig, getSelectedChartStyleIndex } from '../store/app'; +import { connect } from './StateProvider'; + +const { useMemo } = React; + +const chartWrapperStyle = { + // make chartjs chart responsive + position: 'relative', + maxWidth: 1000, + marginTop: '1em', +}; + +const mapState = (s: State) => ({ + apiConfig: getClashAPIConfig(s), + selectedChartStyleIndex: getSelectedChartStyleIndex(s), +}); + +export default connect(mapState)(MemoryChart); + +function MemoryChart({ apiConfig, selectedChartStyleIndex }) { + const ChartMod = chartJSResource.read(); + const memory = fetchData(apiConfig); + const { t } = useTranslation(); + const data = useMemo( + () => ({ + labels: memory.labels, + datasets: [ + { + ...commonDataSetProps, + ...memoryChartOptions, + ...chartStyles[selectedChartStyleIndex].inuse, + label: t('Memory'), + data: memory.inuse, + }, + ], + }), + [memory, selectedChartStyleIndex, t] + ); + + useLineChartMemory(ChartMod.Chart, 'MemoryChart', data, memory); + + return ( + // @ts-expect-error ts-migrate(2322) FIXME: Type '{ position: string; maxWidth: number; }' is ... Remove this comment to see the full error message +
+ +
+ ); +} diff --git a/src/hooks/useLineChart.ts b/src/hooks/useLineChart.ts index 88ee660..62b306b 100644 --- a/src/hooks/useLineChart.ts +++ b/src/hooks/useLineChart.ts @@ -2,6 +2,7 @@ import type { ChartConfiguration } from 'chart.js'; import React from 'react'; import { commonChartOptions } from '~/misc/chart'; +import { memoryChartOptions } from '~/misc/chart-memory'; const { useEffect } = React; @@ -23,3 +24,22 @@ export default function useLineChart( }; }, [chart, elementId, data, subscription, extraChartOptions]); } + +export function useLineChartMemory( + chart: typeof import('chart.js').Chart, + elementId: string, + data: ChartConfiguration['data'], + subscription: any, + extraChartOptions = {} +) { + useEffect(() => { + const ctx = (document.getElementById(elementId) as HTMLCanvasElement).getContext('2d'); + const options = { ...memoryChartOptions, ...extraChartOptions }; + const c = new chart(ctx, { type: 'line', data, options }); + const unsubscribe = subscription && subscription.subscribe(() => c.update()); + return () => { + unsubscribe && unsubscribe(); + c.destroy(); + }; + }, [chart, elementId, data, subscription, extraChartOptions]); +} diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 1fb9d26..0016159 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -11,6 +11,7 @@ export const data = { 'Download Total': '下载总量', 'Active Connections': '活动连接', 'Memory Total': '内存用量', + Memory: '内存', 'Pause Refresh': '暂停刷新', 'Resume Refresh': '继续刷新', close_all_connections: '关闭所有连接', diff --git a/src/misc/chart-memory.ts b/src/misc/chart-memory.ts new file mode 100644 index 0000000..4887b7f --- /dev/null +++ b/src/misc/chart-memory.ts @@ -0,0 +1,64 @@ +import { createAsset } from 'use-asset'; + +import prettyBytes from './pretty-bytes'; +export const chartJSResource = createAsset(() => { + return import('~/misc/chart-lib'); +}); + +export const commonDataSetProps = { borderWidth: 1, pointRadius: 0, tension: 0.2, fill: true }; + +export const memoryChartOptions: import('chart.js').ChartOptions<'line'> = { + responsive: true, + maintainAspectRatio: true, + plugins: { + legend: { labels: { boxWidth: 20 } }, + }, + scales: { + x: { display: false, type: 'category' }, + y: { + type: 'linear', + display: true, + grid: { + display: true, + color: '#555', + drawTicks: false, + }, + border: { + dash: [3, 6], + }, + ticks: { + maxTicksLimit: 3, + callback(value: number) { + return prettyBytes(value); + }, + }, + }, + }, +}; + +export const chartStyles = [ + { + inuse: { + backgroundColor: 'rgba( 116, 162, 249, 0.8)', + borderColor: 'rgb(116, 162, 249)', + }, + }, + { + inuse: { + backgroundColor: 'rgb(98, 190, 100)', + borderColor: 'rgb(78,146,79)', + }, + }, + { + inuse: { + backgroundColor: 'rgba(94, 175, 223, 0.3)', + borderColor: 'rgb(94, 175, 223)', + }, + }, + { + inuse: { + backgroundColor: 'rgba(242, 174, 62, 0.3)', + borderColor: 'rgb(242, 174, 62)', + }, + }, +]; diff --git a/src/misc/chart.ts b/src/misc/chart.ts index 56ffe87..07b1b51 100644 --- a/src/misc/chart.ts +++ b/src/misc/chart.ts @@ -27,6 +27,7 @@ export const commonChartOptions: import('chart.js').ChartOptions<'line'> = { dash: [3, 6], }, ticks: { + maxTicksLimit: 5, callback(value: number) { return prettyBytes(value) + '/s '; }, -- cgit v1.3.1