Skip to content

Added Remix implementation of the TechEmpower Fortunes benchmark #1870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
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
4 changes: 4 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
};
6 changes: 6 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules

/.cache
/build
/public/build
.env
53 changes: 53 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
FROM node:18-alpine AS base

# set for base and all layer that inherit from it
ENV NODE_ENV production

# Install all node_modules, including dev dependencies
FROM base as deps

WORKDIR /myapp

ADD package.json ./
RUN npm install --include=dev

# Setup production node_modules
FROM base as production-deps

WORKDIR /myapp

COPY --from=deps /myapp/node_modules /myapp/node_modules
ADD package.json ./
RUN npm prune --omit=dev

# Build the app
FROM base as build

WORKDIR /myapp

COPY --from=deps /myapp/node_modules /myapp/node_modules

ADD . .
RUN npm run build

# Finally, build the production image with minimal footprint
FROM base

WORKDIR /myapp

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 remix

COPY --from=production-deps --chown=remix:nodejs /myapp/node_modules /myapp/node_modules
COPY --from=build --chown=remix:nodejs /myapp/build /myapp/build
COPY --from=build --chown=remix:nodejs /myapp/public /myapp/public
ADD . .

USER remix

EXPOSE 3000

ENV PORT 3000
ENV DB_HOST postgres_te

CMD ["npm", "start"]
9 changes: 9 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Fortunes implementation using Remix

This is an implementation of the [TechEmpower Fortunes benchmark](https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#fortunes) using Remix

Run the app by executing `npm run build && npm start` in the app root.

The app requires a Postgres database based on the [TechEmpower Postgres Docker image](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/toolset/databases/postgres). Clone the [TechEmpower repo](https://github.com/TechEmpower/FrameworkBenchmarks), navigate to `./toolset/databases/postgres`, and run `docker build -f .\postgres.dockerfile -t postgres_te .` to build a container from that image, then `docker run -p 5432:5432 --name postgres_te postgres_te`.

The [Dockerfile](./Dockerfile) will build a standalone image (based on node-alpine) for running the app on port 3000. Note that in docker the host name for the Postgres database is set to `postgres_te`.
18 changes: 18 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.client
*/

import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
135 changes: 135 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* By default, Remix will handle generating the HTTP Response for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.server
*/

import { PassThrough } from "node:stream";

import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { Response } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5_000;

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext
) {
return isbot(request.headers.get("user-agent"))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}

function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();

responseHeaders.set("Content-Type", "text/html");

resolve(
new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
})
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);

setTimeout(abort, ABORT_DELAY);
});
}

function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();

responseHeaders.set("Content-Type", "text/html");

resolve(
new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
})
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);

setTimeout(abort, ABORT_DELAY);
});
}
79 changes: 79 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/app/models/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import os from "node:os";
import { Pool } from "pg";
import { Fortune } from "./fortune";
import { env } from "process";

const noDb = env.NO_DB ? true : false;
let getFortunes;

if (noDb) {
console.log('Running in "no database" mode');
getFortunes = async function () {
var fortunes = [
new Fortune(1, "fortune: No such file or directory"),
new Fortune(
2,
"A computer scientist is someone who fixes things that aren't broken."
),
new Fortune(3, "After enough decimal places, nobody gives a damn."),
new Fortune(
4,
"A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1"
),
new Fortune(
5,
"A computer program does what you tell it to do, not what you want it to do."
),
new Fortune(
6,
"Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen"
),
new Fortune(7, "Any program that runs right is obsolete."),
new Fortune(
8,
"A list is only as strong as its weakest link. — Donald Knuth"
),
new Fortune(9, "Feature: A bug with seniority."),
new Fortune(10, "Computers make very fast, very accurate mistakes."),
new Fortune(
11,
'<script>alert("This should not be displayed in a browser alert box.");</script>'
),
new Fortune(12, "フレームワークのベンチマーク"),
new Fortune(0, "Additional fortune added at request time."),
];
fortunes.sort((a, b) => a.message.localeCompare(b.message));
return fortunes;
};
} else {
const poolMaxClients = env.DB_MAX_CLIENTS
? parseInt(env.DB_MAX_CLIENTS)
: os.cpus().length;
const dbHost = env.DB_HOST ?? "localhost";
const pool = new Pool({
user: "benchmarkdbuser",
password: "benchmarkdbpass",
database: "hello_world",
host: dbHost,
max: poolMaxClients,
});
const queries = {
fortunes: {
name: "get-fortunes",
text: "SELECT * FROM fortune",
},
};
console.log(`Connected to database ${dbHost}`);
getFortunes = async function () {
const res = await pool.query(queries.fortunes);
var fortunes = res.rows.map((r) => new Fortune(r.id, r.message));
//console.log(`${fortunes.length} rows read from database`);
fortunes.push(new Fortune(0, "Additional fortune added at request time."));
fortunes.sort((a, b) => a.message.localeCompare(b.message));
return fortunes;
};
}

export const db = {
getFortunes: getFortunes,
};
9 changes: 9 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/app/models/fortune.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class Fortune {
constructor(id: number, message: string) {
this.id = id;
this.message = message;
}

id: number;
message: string;
}
31 changes: 31 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";

export const links: LinksFunction = () => [
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
];

export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
41 changes: 41 additions & 0 deletions src/BenchmarksApps/TechEmpower/remix/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { V2_MetaFunction } from "@remix-run/node";

export const meta: V2_MetaFunction = () => {
return [
{ title: "New Remix App" },
{ name: "description", content: "Welcome to Remix!" },
];
};

export default function Index() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
<h1>Welcome to Remix</h1>
<ul>
<li>
<a
target="_blank"
href="https://remix.run/tutorials/blog"
rel="noreferrer"
>
15m Quickstart Blog Tutorial
</a>
</li>
<li>
<a
target="_blank"
href="https://remix.run/tutorials/jokes"
rel="noreferrer"
>
Deep Dive Jokes App Tutorial
</a>
</li>
<li>
<a target="_blank" href="https://remix.run/docs" rel="noreferrer">
Remix Docs
</a>
</li>
</ul>
</div>
);
}
Loading