Skip to content

Commit 4707c91

Browse files
authored
Merge pull request #119 from ford442/jules-5593011313725040260-11c177eb
⚡ Optimize media lookup in MediaPanel
2 parents 36fc549 + 35e953c commit 4707c91

3 files changed

Lines changed: 100 additions & 78 deletions

File tree

benchmark.cjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const count = 10000;
2+
const media = Array.from({ length: count }, (_, i) => ({
3+
id: `id-${i}`,
4+
kind: 'image',
5+
url: `url-${i}`,
6+
fileName: `file-${i}`,
7+
mimeType: 'image/png'
8+
}));
9+
10+
const searchId = `id-${count - 1}`;
11+
12+
console.log(`Benchmarking with ${count} items...`);
13+
14+
console.time('Array.find');
15+
for (let i = 0; i < 10000; i++) {
16+
media.find(m => m.id === searchId);
17+
}
18+
console.timeEnd('Array.find');
19+
20+
const mediaMap = new Map(media.map(m => [m.id, m]));
21+
console.time('Map.get');
22+
for (let i = 0; i < 10000; i++) {
23+
mediaMap.get(searchId);
24+
}
25+
console.timeEnd('Map.get');

components/MediaPanel.tsx

Lines changed: 74 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,74 @@
1-
import React, { useState } from 'react';
2-
import type { MediaItem } from '../types';
3-
4-
interface MediaPanelProps {
5-
media: MediaItem[];
6-
activeMediaId?: string;
7-
onSelect: (id?: string) => void;
8-
onRemove: (id: string) => void;
9-
}
10-
11-
export const MediaPanel: React.FC<MediaPanelProps> = ({ media, activeMediaId, onSelect, onRemove }) => {
12-
const [focusedId, setFocusedId] = useState<string | null>(activeMediaId || null);
13-
14-
const handleOpen = (id: string) => {
15-
setFocusedId(id);
16-
onSelect(id);
17-
};
18-
19-
const handleClose = () => {
20-
setFocusedId(null);
21-
onSelect(undefined);
22-
};
23-
24-
return (
25-
<section className="bg-gray-800 p-4 rounded-lg shadow-lg mb-6">
26-
<h3 className="text-white font-semibold mb-3">Media</h3>
27-
{media.length === 0 ? (
28-
<div className="text-gray-400">No media added yet.</div>
29-
) : (
30-
<div className="grid grid-cols-4 gap-3">
31-
{media.map(item => (
32-
<div key={item.id} className="relative bg-gray-900 rounded overflow-hidden">
33-
{item.kind === 'video' ? (
34-
<video src={item.url} className="w-full h-24 object-cover" muted loop={!!item.loop} playsInline />
35-
) : (
36-
<img src={item.url} alt={item.fileName || 'media'} className="w-full h-24 object-cover" />
37-
)}
38-
39-
<div className="absolute right-1 top-1 flex gap-1">
40-
<button onClick={() => handleOpen(item.id)} className="bg-black/50 text-white px-2 py-1 rounded text-xs">View</button>
41-
<button onClick={() => onRemove(item.id)} className="bg-red-600 text-white px-2 py-1 rounded text-xs">Remove</button>
42-
</div>
43-
</div>
44-
))}
45-
</div>
46-
)}
47-
48-
{focusedId && (
49-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70" onClick={handleClose}>
50-
<div className="bg-black rounded-lg p-4 max-w-5xl max-h-[80vh] w-full" onClick={(e) => e.stopPropagation()}>
51-
<div className="flex justify-between items-start mb-2">
52-
<button onClick={handleClose} className="text-gray-300">Close</button>
53-
</div>
54-
<div className="flex items-center justify-center">
55-
{(() => {
56-
const item = media.find(m => m.id === focusedId);
57-
if (!item) return null;
58-
if (item.kind === 'video') {
59-
return (
60-
<video src={item.url} controls autoPlay muted={!!item.muted} loop={!!item.loop} className="max-h-[70vh] w-auto max-w-full" />
61-
);
62-
}
63-
// images & gifs
64-
return <img src={item.url} alt={item.fileName || 'media'} className="max-h-[70vh] w-auto max-w-full" />;
65-
})()}
66-
</div>
67-
</div>
68-
</div>
69-
)}
70-
</section>
71-
);
72-
};
73-
1+
import React, { useState, useMemo } from 'react';
2+
import type { MediaItem } from '../types';
3+
4+
interface MediaPanelProps {
5+
media: MediaItem[];
6+
activeMediaId?: string;
7+
onSelect: (id?: string) => void;
8+
onRemove: (id: string) => void;
9+
}
10+
11+
export const MediaPanel: React.FC<MediaPanelProps> = ({ media, activeMediaId, onSelect, onRemove }) => {
12+
const [focusedId, setFocusedId] = useState<string | null>(activeMediaId || null);
13+
14+
const mediaMap = useMemo(() => new Map(media.map(item => [item.id, item])), [media]);
15+
16+
const handleOpen = (id: string) => {
17+
setFocusedId(id);
18+
onSelect(id);
19+
};
20+
21+
const handleClose = () => {
22+
setFocusedId(null);
23+
onSelect(undefined);
24+
};
25+
26+
return (
27+
<section className="bg-gray-800 p-4 rounded-lg shadow-lg mb-6">
28+
<h3 className="text-white font-semibold mb-3">Media</h3>
29+
{media.length === 0 ? (
30+
<div className="text-gray-400">No media added yet.</div>
31+
) : (
32+
<div className="grid grid-cols-4 gap-3">
33+
{media.map(item => (
34+
<div key={item.id} className="relative bg-gray-900 rounded overflow-hidden">
35+
{item.kind === 'video' ? (
36+
<video src={item.url} className="w-full h-24 object-cover" muted loop={!!item.loop} playsInline />
37+
) : (
38+
<img src={item.url} alt={item.fileName || 'media'} className="w-full h-24 object-cover" />
39+
)}
40+
41+
<div className="absolute right-1 top-1 flex gap-1">
42+
<button onClick={() => handleOpen(item.id)} className="bg-black/50 text-white px-2 py-1 rounded text-xs">View</button>
43+
<button onClick={() => onRemove(item.id)} className="bg-red-600 text-white px-2 py-1 rounded text-xs">Remove</button>
44+
</div>
45+
</div>
46+
))}
47+
</div>
48+
)}
49+
50+
{focusedId && (
51+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70" onClick={handleClose}>
52+
<div className="bg-black rounded-lg p-4 max-w-5xl max-h-[80vh] w-full" onClick={(e) => e.stopPropagation()}>
53+
<div className="flex justify-between items-start mb-2">
54+
<button onClick={handleClose} className="text-gray-300">Close</button>
55+
</div>
56+
<div className="flex items-center justify-center">
57+
{(() => {
58+
const item = mediaMap.get(focusedId);
59+
if (!item) return null;
60+
if (item.kind === 'video') {
61+
return (
62+
<video src={item.url} controls autoPlay muted={!!item.muted} loop={!!item.loop} className="max-h-[70vh] w-auto max-w-full" />
63+
);
64+
}
65+
// images & gifs
66+
return <img src={item.url} alt={item.fileName || 'media'} className="max-h-[70vh] w-auto max-w-full" />;
67+
})()}
68+
</div>
69+
</div>
70+
</div>
71+
)}
72+
</section>
73+
);
74+
};

hooks/useWebGPURender.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -370,16 +370,12 @@ export function useWebGPURender(
370370
useEffect(() => {
371371
const device = deviceRef.current;
372372
if (!device || !gpuReady) return;
373+
const p = renderParamsRef.current;
373374
console.log(`[PatternDisplay] Updating cells buffer: matrix=${matrix ? 'yes' : 'null'}, rows=${matrix?.numRows}, channels=${matrix?.numChannels}`);
374375
if (cellsBufferRef.current) cellsBufferRef.current.destroy();
375376
const isHighPrec = shaderFile.includes('v0.36') || shaderFile.includes('v0.37') || shaderFile.includes('v0.38') || shaderFile.includes('v0.39') || shaderFile.includes('v0.40') || shaderFile.includes('v0.42') || shaderFile.includes('v0.43') || shaderFile.includes('v0.44') || shaderFile.includes('v0.45') || shaderFile.includes('v0.46') || shaderFile.includes('v0.47') || shaderFile.includes('v0.48') || shaderFile.includes('v0.49') || shaderFile.includes('v0.50');
376377
const packFunc = isHighPrec ? packPatternMatrixHighPrecision : packPatternMatrix;
377378
const { packedData, noteCount } = packFunc(p.matrix, p.padTopChannel);
378-
let noteCount = 0;
379-
for (let i = 0; i < packedData.length; i += 2) {
380-
const note = ((packedData[i] ?? 0) >> 24) & 0xFF;
381-
if (note > 0) noteCount++;
382-
}
383379
console.log(`[PatternDisplay] Packed data contains ${noteCount} notes in ${packedData.length / 2} cells`);
384380
cellsBufferRef.current = createBufferWithData(device, packedData, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
385381
if (layoutTypeRef.current === 'extended') {

0 commit comments

Comments
 (0)