Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3296426
add: aggressive cleanup.
ItzNotABug Nov 12, 2025
6001954
Merge branch 'tests' into 'aggressive-improvements'.
ItzNotABug Nov 12, 2025
7bb74d0
fixes and lint.
ItzNotABug Nov 12, 2025
29e89ea
address comment.
ItzNotABug Nov 13, 2025
a81fad6
Merge branch 'main' into aggressive-improvements
ItzNotABug Nov 13, 2025
0c569bf
remove: more unneeded files.
ItzNotABug Nov 16, 2025
6e8a2f9
use: chrome-headless and bun debian!
ItzNotABug Nov 16, 2025
e4e61bd
Clean up unused modules from node_modules
ItzNotABug Nov 16, 2025
50808d8
update: improve the speed!
ItzNotABug Nov 16, 2025
28e8118
use: specific image version!
ItzNotABug Nov 16, 2025
e7ba28c
Update base image version in Dockerfile
ItzNotABug Nov 16, 2025
4e6b20d
add: proper, nicer router support.
ItzNotABug Nov 16, 2025
8e055b6
Merge branch 'aggressive-improvements' into improve-screenshots-perf
ItzNotABug Nov 16, 2025
f999d75
update: address commment from rabbit.
ItzNotABug Nov 16, 2025
ff2bc5b
Merge branch 'aggressive-improvements' into improve-screenshots-perf
ItzNotABug Nov 16, 2025
f8f05c8
add: test stats to post on PR.
ItzNotABug Nov 16, 2025
7cee01d
fix: ci.
ItzNotABug Nov 16, 2025
8575c8d
fix: ci.
ItzNotABug Nov 16, 2025
4204a3c
update: ci.
ItzNotABug Nov 16, 2025
231a696
update: ci for comment spam.
ItzNotABug Nov 16, 2025
e51309c
Merge pull request #13 from appwrite/add-stats-on-pr
ItzNotABug Nov 16, 2025
0580dac
fix: startup time.
ItzNotABug Nov 16, 2025
68decdf
Apply suggestion from @ItzNotABug
ItzNotABug Nov 16, 2025
b614f34
Merge branch 'aggressive-improvements' into improve-screenshots-perf
ItzNotABug Nov 16, 2025
77cd303
lint.
ItzNotABug Nov 16, 2025
b913791
Merge pull request #12 from appwrite/improve-screenshots-perf
eldadfux Nov 20, 2025
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ LICENSE
test-screenshots/
comparison/
.lighthouse/
lighthouse/

# Build output
dist/
Expand Down
62 changes: 61 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ jobs:
tests:
name: "Unit and E2E Tests"
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -28,10 +31,16 @@ jobs:
run: docker compose up -d

- name: Wait for container to be ready
id: container-startup
run: |
START_TIME=$(date +%s%3N)
for i in {1..30}; do
if curl -f http://localhost:3000/v1/health > /dev/null 2>&1; then
echo "Container is ready!"
END_TIME=$(date +%s%3N)
STARTUP_TIME_MS=$((END_TIME - START_TIME))
STARTUP_TIME=$(echo "scale=2; $STARTUP_TIME_MS / 1000" | bc)
echo "startup_time=$STARTUP_TIME" >> $GITHUB_OUTPUT
echo "Container is ready in ${STARTUP_TIME}s!"
exit 0
fi
echo "Waiting for container... ($i/30)"
Expand All @@ -42,6 +51,57 @@ jobs:
docker compose logs
exit 1

- name: Collect Docker stats
if: github.event_name == 'pull_request'
continue-on-error: true
id: docker-stats
run: |
# Get image size
IMAGE_SIZE=$(docker images appwrite/browser:local --format "{{.Size}}")

# Get container stats
CONTAINER_ID=$(docker compose ps -q appwrite-browser)
MEMORY_USAGE=$(docker stats $CONTAINER_ID --no-stream --format "{{.MemUsage}}" | cut -d'/' -f1 | xargs)

# Quick screenshot benchmark (3 runs, average)
TOTAL=0
for i in {1..3}; do
START=$(date +%s%3N)
curl -s -X POST http://localhost:3000/v1/screenshots \
-H "Content-Type: application/json" \
-d '{"url":"https://appwrite.io"}' \
-o /dev/null
END=$(date +%s%3N)
DURATION=$((END - START))
TOTAL=$((TOTAL + DURATION))
done
SCREENSHOT_AVG_MS=$((TOTAL / 3))
SCREENSHOT_AVG=$(echo "scale=2; $SCREENSHOT_AVG_MS / 1000" | bc)

# Store in GitHub output
echo "image_size=$IMAGE_SIZE" >> $GITHUB_OUTPUT
echo "memory_usage=$MEMORY_USAGE" >> $GITHUB_OUTPUT
echo "screenshot_time=$SCREENSHOT_AVG" >> $GITHUB_OUTPUT

- name: Comment PR with stats
if: github.event_name == 'pull_request' && steps.docker-stats.outcome == 'success'
continue-on-error: true
uses: marocchino/sticky-pull-request-comment@v2
with:
header: docker-image-stats
skip_unchanged: true
message: |
## Docker Image Stats

| Metric | Value |
|--------|-------|
| Image Size | ${{ steps.docker-stats.outputs.image_size }} |
| Memory Usage | ${{ steps.docker-stats.outputs.memory_usage }} |
| Cold Start Time | ${{ steps.container-startup.outputs.startup_time }}s |
| Screenshot Time | ${{ steps.docker-stats.outputs.screenshot_time }}s |

<sub>Screenshot benchmark: Average of 3 runs on https://appwrite.io</sub>

- name: Run e2e tests
run: bun test:e2e

Expand Down
48 changes: 27 additions & 21 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,41 +1,47 @@
FROM oven/bun:1.3.2-alpine AS base
# debian so we can re-use!
FROM oven/bun:1.3.2-debian AS base

WORKDIR /app

COPY package.json bun.lock ./
COPY src/utils/clean-modules.ts ./src/utils/clean-modules.ts

RUN bun install --frozen-lockfile --production && \
bun run ./src/utils/clean-modules.ts && \
rm -rf ~/.bun/install/cache /tmp/*

FROM oven/bun:1.3.2-alpine AS final
# well-known OSS docker image
FROM chromedp/headless-shell:143.0.7445.3 AS final

RUN apk upgrade --no-cache --available && \
apk add --no-cache \
chromium \
ttf-freefont \
font-noto-emoji \
tini && \
apk add --no-cache font-wqy-zenhei --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community && \
# remove unnecessary chromium files to save space
rm -rf /usr/lib/chromium/chrome_200_percent.pak \
/usr/lib/chromium/chrome_100_percent.pak \
/usr/lib/chromium/xdg-mime \
/usr/lib/chromium/xdg-settings \
/usr/lib/chromium/chrome-sandbox
# install fonts only
RUN apt-get update && \
apt-get install -y --no-install-recommends \
tini \
ca-certificates \
fonts-liberation \
fonts-noto-color-emoji \
fonts-wqy-zenhei && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/apt/archives/*

RUN addgroup -S chrome && adduser -S -G chrome chrome
# copy bun from debian base above!
COPY --from=base /usr/local/bin/bun /usr/local/bin/bun

# Add chrome user
RUN groupadd -r chrome && useradd -r -g chrome chrome

ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser \
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/headless-shell/headless-shell \
NODE_ENV=production

WORKDIR /app

COPY package.json ./
COPY --from=base /app/node_modules ./node_modules
COPY src/ ./src/
COPY --chown=chrome:chrome src/ ./src/
COPY --chown=chrome:chrome package.json ./
COPY --chown=chrome:chrome --from=base /app/node_modules ./node_modules

RUN chown -R chrome:chrome /app
# for e2e tests and `reports` endpoint!
RUN install -d -o chrome -g chrome lighthouse

USER chrome

Expand Down
1 change: 1 addition & 0 deletions bun.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "appwrite-browser",
Expand Down
2 changes: 1 addition & 1 deletion src/schemas/screenshot.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const screenshotSchema = z.object({
url: z.string().url(),
theme: z.enum(["light", "dark"]).default("light"),
headers: z.record(z.string(), z.any()).optional(),
sleep: z.number().min(0).max(60000).default(3000),
sleep: z.number().min(0).max(60000).default(0),
// Viewport options
viewport: z
.object({
Expand Down
32 changes: 8 additions & 24 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,17 @@ import {
handleScreenshotsRequest,
handleTestRequest,
} from "./routes";
import { Router } from "./utils/router";

const router = new Router();
router.add("POST", "/v1/screenshots", handleScreenshotsRequest);
router.add("POST", "/v1/reports", handleReportsRequest);
router.add("GET", "/v1/health", handleHealthRequest);
router.add("GET", "/v1/test", handleTestRequest);

const server = Bun.serve({
port,
async fetch(req) {
const url = new URL(req.url);
const path = url.pathname;

// Route matching
if (path === "/v1/screenshots" && req.method === "POST") {
return await handleScreenshotsRequest(req);
}

if (path === "/v1/reports" && req.method === "POST") {
return await handleReportsRequest(req);
}

if (path === "/v1/health" && req.method === "GET") {
return await handleHealthRequest(req);
}

if (path === "/v1/test" && req.method === "GET") {
return await handleTestRequest(req);
}

// 404 Not Found
return new Response("Not Found", { status: 404 });
},
fetch: (request) => router.handle(request),
});

console.log(`Server running on http://0.0.0.0:${server.port}`);
Loading