improve: performance, added a fps monitor panel

This commit is contained in:
alikia2x (寒寒) 2025-02-04 22:25:40 +08:00
parent 74a21e721d
commit 7aef8e873d
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
9 changed files with 203 additions and 30 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -4,7 +4,7 @@
"private": false,
"module": "index.ts",
"type": "module",
"workspaces": ["packages/web", "packages/core", "packages/electron"],
"workspaces": ["packages/web", "packages/core"],
"scripts": {
"electron:dev": "bun --filter 'electron' dev",
"web:dev": "bun --filter 'web' dev",
@ -37,7 +37,6 @@
"concurrently": "^9.0.1",
"cross-env": "^7.0.3"
},
"dependencies": {},
"trustedDependencies": [
"svelte-preprocess"
]

View File

@ -0,0 +1,135 @@
<script lang="ts">
import { onMount } from 'svelte';
import Chart from 'chart.js/auto';
// 可调整的更新频率(毫秒)
const UPDATE_INTERVAL = 300;
const TIME_WINDOW = 1000 * 15;
let frameCount = 0;
let lastTime = 0;
let fps = 0;
let frameTimes: number[] = [];
let frameTime = 0;
let onePercentLow = 0;
let fpsChart: Chart;
let frameTimeChart: Chart;
let lastFrameTime = 0;
function updateFPS() {
const currentTime = performance.now();
const deltaTime = currentTime - lastTime;
frameTime = currentTime - lastFrameTime;
// 计算1% Low FPS
const sortedFrameTimes = frameTimes.sort((a, b) => b - a);
const onePercentIndex = Math.floor(sortedFrameTimes.length * 0.01);
onePercentLow = Math.round(1000 / sortedFrameTimes[onePercentIndex]);
if (frameTimeChart) {
if ((frameTimeChart.data.labels![0] as number) < Date.now() - 5000) {
frameTimeChart.data.labels!.shift();
frameTimeChart.data.datasets[0].data.shift();
}
frameTimeChart.data.labels!.push(Date.now());
frameTimeChart.data.datasets[0].data.push(frameTime);
}
if (deltaTime > UPDATE_INTERVAL) {
fps = Math.round((frameCount * 1000) / deltaTime);
// 更新图表数据
if (fpsChart) {
if ((fpsChart.data.labels![0] as number) < Date.now() - TIME_WINDOW) {
fpsChart.data.labels!.shift();
fpsChart.data.datasets[0].data.shift();
}
fpsChart.data.labels!.push(Date.now());
fpsChart.data.datasets[0].data.push(fps);
fpsChart.update();
}
if (frameTimeChart) {
frameTimeChart.update();
}
frameCount = 0;
lastTime = currentTime;
}
frameCount++;
frameTimes.push(frameTime);
lastFrameTime = performance.now();
requestAnimationFrame(updateFPS);
}
const createChart = (ctx: CanvasRenderingContext2D, label: string, color: string) => {
return new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: label,
borderColor: color,
data: [],
pointRadius: 0,
borderWidth: label == 'FPS' ? undefined : 2,
tension: label == 'FPS' ? 0.1 : 0,
}
]
},
options: {
scales: {
x: {
display: false
},
y: {
beginAtZero: false,
ticks: {
padding: 0
},
border: {
display: false
}
}
},
plugins: {
legend: {
display: false
}
},
interaction: {
mode: 'x'
},
animations: {
y: {
duration: 0
},
x: {
duration: label == 'FPS' ? undefined : 0,
}
}
}
});
};
onMount(() => {
const ctx1 = (document.getElementById('fpsChart')! as HTMLCanvasElement).getContext('2d')!;
const ctx2 = (document.getElementById('frameTimeChart')! as HTMLCanvasElement).getContext('2d')!;
fpsChart = createChart(ctx1, 'FPS', '#4CAF50');
frameTimeChart = createChart(ctx2, 'Frame Time', '#2196F3');
updateFPS();
});
</script>
<p>
<span>{fps} fps</span><br />
<span>Frame Time: {frameTime.toFixed(2)} ms</span><br />
<span>1% Low: {onePercentLow} fps</span>
</p>
<span class="fixed right-2 text-white/50">fps</span>
<canvas id="fpsChart" width="400" height="100" class="mt-2"></canvas>
<span class="fixed right-2 text-white/50">frametime</span>
<canvas id="frameTimeChart" width="400" height="100" class="mt-2"></canvas>

View File

@ -38,6 +38,7 @@
let isCurrentLyric = false;
function updateY(timestamp: number) {
if (stopped) return;
if (lastUpdateY === undefined) {
lastUpdateY = new Date().getTime();
}
@ -52,6 +53,7 @@
}
function updateX(timestamp: number) {
if (stopped) return;
if (lastUpdateX === undefined) {
lastUpdateX = timestamp;
}
@ -70,6 +72,7 @@
* @param {number} pos - X offset, in pixels
*/
export const setX = (pos: number) => {
stopped = true;
positionX = pos;
};
@ -78,6 +81,7 @@
* @param {number} pos - Y offset, in pixels
*/
export const setY = (pos: number) => {
stopped = true;
positionY = pos;
};

View File

@ -4,6 +4,7 @@
import LyricLine from './lyricLine.svelte';
import createLyricsSearcher from '@core/lyrics/lyricSearcher';
import userAdjustingProgress from '@core/state/userAdjustingProgress';
import DisplayFps from '../displayFPS.svelte';
// constants
const viewportHeight = document.documentElement.clientHeight;
@ -16,11 +17,16 @@
document.body.style.overflow = 'hidden';
// Props
let { originalLyrics, progress, player, showInteractiveBox } : {
originalLyrics: LyricData,
progress: number,
player: HTMLAudioElement | null,
showInteractiveBox: boolean
let {
originalLyrics,
progress,
player,
showInteractiveBox
}: {
originalLyrics: LyricData;
progress: number;
player: HTMLAudioElement | null;
showInteractiveBox: boolean;
} = $props();
// States
@ -76,15 +82,28 @@
for (let i = 0; i < lyricElements.length; i++) {
const currentLyricComponent = lyricComponents[i];
const lyric = originalLyrics.scripts[i];
const lyricBeforeProgress = lyric.end < progress;
const lyricInProgress = lyric.start <= progress && progress <= lyric.end;
const lyricWillHigherThanViewport = lyricTopList[i + 3] - relativeOrigin < 0;
const lyricWillLowerThanViewport = lyricTopList[i - 3] - relativeOrigin > lyricsContainer?.getBoundingClientRect().height!;
let delay = 0;
if (progress > lyric.end) {
if (lyricBeforeProgress) {
delay = 0;
} else if (lyric.start <= progress && progress <= lyric.end) {
delay = 0.042;
} else if (lyricInProgress) {
delay = 0.03;
} else {
delay = Math.min(Math.min(currentLyricDuration, 0.6), 0.067 * (i - currentLyricIndex + 1.2));
}
currentLyricComponent.update({ x: 0, y: lyricTopList[i] - relativeOrigin }, delay);
// if it's not in the viewport, we need to use animations
if (lyricWillHigherThanViewport || lyricWillLowerThanViewport) {
currentLyricComponent.update({ x: 0, y: lyricTopList[i] - relativeOrigin }, delay);
}
// if it's still in the viewport, we need to use spring animation
else {
currentLyricComponent.update({ x: 0, y: lyricTopList[i] - relativeOrigin }, delay);
}
}
}
@ -111,11 +130,16 @@
});
function handleScroll(deltaY: number) {
if (lyricComponents[0].getInfo().y > 0 && deltaY < 0) {
deltaY = 0;
}
if (lyricComponents[lyricComponents.length - 1].getInfo().y < 100 && deltaY > 0) {
deltaY = 0;
}
for (let i = 0; i < lyricElements.length; i++) {
const currentLyricComponent = lyricComponents[i];
const currentY = currentLyricComponent.getInfo().y;
scrolling = true;
currentLyricComponent.stop();
currentLyricComponent.setY(currentY - deltaY);
currentLyricComponent.syncSpringWithDelta(deltaY);
}
@ -200,21 +224,19 @@
lastEventProgress = progress;
if (!lyricChanged || scrolling) return;
if (!lyricIndexDeltaTooBig && deltaInRange) {
console.log("Event: regular move");
console.log('Event: regular move');
console.log(new Date().getTime(), lastSeekForward);
computeLayout();
}
else if ($userAdjustingProgress) {
} else if ($userAdjustingProgress) {
if (deltaTooBig && lyricChanged) {
console.log("Event: seek forward");
console.log('Event: seek forward');
seekForward();
} else if (deltaIsNegative && lyricChanged) {
console.log("Event: seek backward");
console.log('Event: seek backward');
seekForward();
}
}
else {
console.log("Event: regular move");
} else {
console.log('Event: regular move');
computeLayout();
}
});
@ -253,16 +275,23 @@
{#if debugMode}
<span
class="text-white text-lg absolute z-50 px-2 py-0.5 m-2 rounded-3xl bg-white bg-opacity-20 backdrop-blur-lg
right-0 font-mono">
right-0 font-mono"
>
progress: {progress.toFixed(2)}, nextUpdate: {nextUpdate}, scrolling: {scrolling}, current: {currentLyricIndex},
uap: {$userAdjustingProgress}
</span>
<div
class="text-black/80 text-sm absolute z-50 px-3 py-2 m-2 rounded-lg bg-white/30 backdrop-blur-xl
left-0 font-mono"
>
<DisplayFps />
</div>
{/if}
{#if originalLyrics && originalLyrics.scripts}
<div
class={`absolute top-[6.5rem] md:top-36 xl:top-0 w-screen xl:w-[52vw] px-6 md:px-12 duration-500
${showInteractiveBox ? "h-[calc(100vh-21rem)]" : "h-[calc(100vh-7rem)]"}
${showInteractiveBox ? 'h-[calc(100vh-21rem)]' : 'h-[calc(100vh-7rem)]'}
lg:px-[7.5rem] xl:left-[46vw] xl:px-[3vw] xl:h-screen font-sans
text-left no-scrollbar z-[1] pt-16 overflow-hidden`}
style={`mask: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 7%, rgba(0, 0, 0, 1) 95%,
@ -270,8 +299,16 @@
bind:this={lyricsContainer}
>
{#each lyricLines as lyric, i}
<LyricLine line={lyric} index={i} bind:this={lyricComponents[i]} {debugMode} {lyricClick} {progress}
{currentLyricIndex} {scrolling}/>
<LyricLine
line={lyric}
index={i}
bind:this={lyricComponents[i]}
{debugMode}
{lyricClick}
{progress}
{currentLyricIndex}
{scrolling}
/>
{/each}
</div>
{/if}

View File

@ -27,13 +27,12 @@
},
"dependencies": {
"@alikia/aqualyrics": "npm:@jsr/alikia__aqualyrics",
"@applemusic-like-lyrics/core": "^0.1.3",
"@applemusic-like-lyrics/lyric": "^0.2.2",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@types/bun": "^1.1.6",
"bezier-easing": "^2.1.0",
"chart.js": "^4.4.7",
"jotai": "^2.8.0",
"jotai-svelte": "^0.0.2",
"jss": "^10.10.0",

View File

@ -37,8 +37,6 @@
},
"dependencies": {
"@alikia/aqualyrics": "npm:@jsr/alikia__aqualyrics",
"@applemusic-like-lyrics/core": "^0.1.3",
"@applemusic-like-lyrics/lyric": "^0.2.4",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",

View File

@ -209,7 +209,7 @@
if (audioPlayer === null) return;
if ($userAdjustingProgress === false) currentProgress = audioPlayer.currentTime;
progressBarRaw.set(audioPlayer.currentTime);
}, 50);
}, 20);
}
onMount(() => {

View File

@ -13,7 +13,8 @@
"sourceMap": true,
"strict": true,
"types": ["bun"]
}
},
"exclude": ["node_modules", "build", "dist", "tmp"]
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//