Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Measure resolve duration

on:
workflow_dispatch:

env:
BENCHMARK_REPO: software-mansion-labs/typegpu-benchmarker

jobs:
measure:
runs-on: ubuntu-latest

steps:
- name: Clone benchmarking repo
uses: actions/checkout@v5
with:
repository: ${{ env.BENCHMARK_REPO }}
ref: main
token: ${{ secrets.BENCHMARKER_REPO_ACCESS_TOKEN }}

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false

- name: Install Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'

- name: Install deps
run: |
pnpm install --frozen-lockfile
- name: Install Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Run benchmarks
run: |
pnpm run measure
- name: Save benchmark results across the jobs
uses: actions/upload-artifact@v4
with:
name: data
path: benchmarks
plot:
runs-on: ubuntu-latest
needs: measure

steps:
- name: Clone benchmarking repo
uses: actions/checkout@v5
with:
repository: ${{ env.BENCHMARK_REPO }}
ref: main
token: ${{ secrets.BENCHMARKER_REPO_ACCESS_TOKEN }}

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false

- name: Download benchmark data
uses: actions/download-artifact@v4
with:
name: data
path: benchmarks

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '>=3.11'

- name: Create venv
run: |
python3 -m venv .venv
- name: Plot
shell: bash
run: |
. .venv/bin/activate
pip install -r requirements.txt
pnpm run plot
- name: Commit and push results and plot
run: |
git config user.name "github-actions"
git config user.email "[email protected]"
git add .
git commit -m "Automated benchmark update" || echo "No changes"
git push origin main
123 changes: 123 additions & 0 deletions apps/typegpu-docs/src/components/resolve/PlotGallery.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little over-engineered but since it's for dev I don't really mind it (all the comments are just general feedback for learning purposes).

  • We treat plots like they are not constant but they are so we should use it (for example there is no need to do useMemo for the plots - if we wanted to do extended plots we could just do that out of the component)
  • There is no expensive calculation going on here so the optimizations are a little overkill but good practice
  • Rest of the feedback is in the other comments

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the feedback. I deleted useMemo, but I can't change isTransitioning to useRef. isTransitioning state plays a crucial role in smooth carousel wrap when we transition from the last plot -> to the first plot (and first -> last).

  1. Because plots are extended with 'guard' plots, the animation appears to move continuously to the right (left).
  2. Then, I handle the transform property. I set isTransitioning to false, and change the index to real plot (not the guarding one)
  3. The CSS (this one containing animation logic) rerenders and we don't see the carousel going through all the plots.

Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';

const plots = [
'https://raw.githubusercontent.com/software-mansion-labs/typegpu-benchmarker/main/plots/combined-resolveDuration-full.png',
'https://raw.githubusercontent.com/software-mansion-labs/typegpu-benchmarker/main/plots/combined-resolveDuration-full-log.png',
'https://raw.githubusercontent.com/software-mansion-labs/typegpu-benchmarker/main/plots/combined-resolveDuration-latest5.png',
'https://raw.githubusercontent.com/software-mansion-labs/typegpu-benchmarker/main/plots/combined-resolveDuration-under10k.png',
];
const slideCount = plots.length;
const extendedPlots = [plots[slideCount - 1], ...plots, plots[0]];
const extendedSlideCount = extendedPlots.length;

function PlotSlide({ url }: { url: string }) {
return (
<div className='flex h-[60vh] max-h-[50vw] w-full flex-shrink-0 justify-center'>
<img
className='h-full rounded-2xl object-contain'
src={`${url}?t=${Date.now()}`} // TODO: proper versioning ;)
alt={`${new URL(url).pathname.split('/').pop()}`}
/>
</div>
);
}

const buttonUtilityClasses =
'-translate-y-1/2 absolute top-1/2 rounded-full border border-gray-700 bg-gray-800 p-4 text-gray-150 transition-all duration-300 ease-in-out hover:bg-gray-700 hover:text-white active:bg-gray-500 active:text-white z-1';
const chevronUtilityClasses = 'w-4 h-4 sm:w-8 sm:h-8';

export default function PlotGallery() {
const [currentIndex, setCurrentIndex] = useState(1);
const [isTransitioning, setIsTransitioning] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [isTransitioning, setIsTransitioning] = useState(false);
const isTransitioningRef = useRef(false);

I would go for this - isTransitioning does not need to cause re-renders and it will simplify transition handling


const nextSlide = useCallback((isTransitioning: boolean) => {
if (isTransitioning) return;
setIsTransitioning(true);
setCurrentIndex((prev) => prev + 1); // to avoid deps
}, []);
Comment on lines +34 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+ const slideCount = plots.length;
Suggested change
const nextSlide = useCallback((isTransitioning: boolean) => {
if (isTransitioning) return;
setIsTransitioning(true);
setCurrentIndex((prev) => prev + 1); // to avoid deps
}, []);
const nextSlide = useCallback(() => {
if (isTransitioningRef.current) return;
isTransitioningRef.current = true;
setCurrentIndex((prev) => prev === slideCount - 1 ? 0 : prev + 1);
}, [slideCount]);


const prevSlide = useCallback((isTransitioning: boolean) => {
if (isTransitioning) return;
setIsTransitioning(true);
setCurrentIndex((prev) => prev - 1);
}, []);

const handleTransitionEnd = useCallback((index: number) => {
setIsTransitioning(false);
if (index === 0) {
setCurrentIndex(slideCount);
} else if (index === extendedSlideCount - 1) {
setCurrentIndex(1);
}
}, []);

const goToSlide = useCallback((index: number, isTransitioning: boolean) => {
if (isTransitioning) return;
setIsTransitioning(true);
setCurrentIndex(index + 1);
}, []);

const getActualIndex = (): number => {
if (currentIndex === 0) return slideCount - 1;
if (currentIndex === extendedSlideCount - 1) return 0;
return currentIndex - 1;
};

useEffect(() => {
// TODO: add touch handling
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'ArrowLeft') prevSlide(isTransitioning);
if (event.key === 'ArrowRight') nextSlide(isTransitioning);
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [prevSlide, nextSlide, isTransitioning]);

return (
<div className='relative flex-grow overflow-hidden'>
<button
className={`left-4 ${buttonUtilityClasses}`}
type='button'
onClick={() => prevSlide(isTransitioning)}
>
<ChevronLeft className={chevronUtilityClasses} />
</button>

<button
className={`right-4 ${buttonUtilityClasses}`}
type='button'
onClick={() => nextSlide(isTransitioning)}
>
<ChevronRight className={chevronUtilityClasses} />
</button>

<div
className={`flex h-full w-full transition-transform duration-200 ease-in-out ${
isTransitioning ? '' : 'transition-none' // this is necessary for smooth wrapping
}`}
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
onTransitionEnd={() => handleTransitionEnd(currentIndex)}
>
{extendedPlots.map((url, index) => (
<PlotSlide key={`slide-${index}-${url}`} url={url} />
))}
</div>

<div className='-translate-x-1/2 absolute bottom-17 left-1/2 flex space-x-3'>
{plots.map((url, index) => (
<button
key={`dot-${index}-${url}`}
type='button'
onClick={() => goToSlide(index, isTransitioning)}
className={`h-4 w-4 rounded-full transition-all duration-200 ease-in-out ${
index === getActualIndex()
? 'scale-125 bg-gray-500'
: 'bg-gray-800 hover:bg-gray-700'
}`}
/>
))}
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions apps/typegpu-docs/src/pages/resolve/index.astro
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that the page does not respond to the light theme, but /benchmark acts the same so we may ignore it (at least until the landing page rework)

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
import { Image } from "astro:assets";
import PageLayout from "../../layouts/PageLayout.astro";
import PlotGallery from "../../components/resolve/PlotGallery.tsx";
import TypeGPULogoDark from "../../assets/typegpu-logo-dark.svg";
---

<PageLayout title="TypeGPU functions resolve complexity | TypeGPU" theme="dark">
<main class="h-[100dvh] flex flex-col">
<h1 class="flex items-center justify-center">
<a href="/TypeGPU">
<Image src={TypeGPULogoDark} alt="TypeGPU Logo" class="w-40" />
</a>
<p class="text-lg font-sans text-gray-300">— resolve complexity</p>
</h1>

<PlotGallery client:only="react" />
</main>
</PageLayout>