Skip to content

Commit

Permalink
Accidental improvements whilst helping Jem on the docs... (#2067)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjie authored May 14, 2024
2 parents 8f578e8 + 2d993d9 commit b901cf0
Show file tree
Hide file tree
Showing 24 changed files with 506 additions and 146 deletions.
5 changes: 5 additions & 0 deletions .changeset/curvy-nails-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"grafast": patch
---

Add `nodeIdFromNode()` helper
9 changes: 9 additions & 0 deletions .changeset/lemon-spoons-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"grafast": patch
---

Fix performance issue in `loadOne()`/`loadMany()` due to using
`setTimeout(cb, 0)`, now using `process.nextTick(cb)`. High enough concurrency
and the issue goes away, but with limited concurrency this causes a lot of
`(idle)` in profiling and thus completing 10k items took longer. (Lots of time
spent in `epoll_pwait`.)
6 changes: 6 additions & 0 deletions .changeset/tough-carrots-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"grafast": patch
---

Allow applying `if` to `inhibitOnNull` - e.g. only inhibit on null if some other
condition matches.
26 changes: 9 additions & 17 deletions grafast/grafast/src/engine/LayerPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,15 @@ import {
FLAG_INHIBITED,
FLAG_NULL,
FORBIDDEN_BY_NULLABLE_BOUNDARY_FLAGS,
NO_FLAGS,
} from "../interfaces.js";
import { resolveType } from "../polymorphic.js";
import type {
ExecutableStep,
ModifierStep,
UnbatchedExecutableStep,
} from "../step";
import {
batchExecutionValue,
newBucket,
unaryExecutionValue,
} from "./executeBucket.js";
import { batchExecutionValue, newBucket } from "./executeBucket.js";
import type { OperationPlan } from "./OperationPlan";

/*
Expand Down Expand Up @@ -519,19 +516,14 @@ export class LayerPlan<TReason extends LayerPlanReason = LayerPlanReason> {
}
const itemStepId = this.rootStep.id;
// Item steps are **NOT** unary
const itemStepIdList: any[] | null = this.rootStep._isUnary ? null : [];
if (this.rootStep._isUnary) {
// handled later
const list = listStepStore.at(0);
store.set(
itemStepId,
unaryExecutionValue(Array.isArray(list) ? list[0] : list),
);
} else {
store.set(itemStepId, batchExecutionValue(itemStepIdList!));
throw new Error("listItem layer plan can't have a unary root step!");
}
const ev = batchExecutionValue([] as any[]);
store.set(itemStepId, ev);

for (const stepId of copyStepIds) {
// Deliberate shadowing
const ev = parentBucket.store.get(stepId)!;
if (ev.isBatch) {
// Prepare store with an empty list for each copyPlanId
Expand All @@ -556,9 +548,9 @@ export class LayerPlan<TReason extends LayerPlanReason = LayerPlanReason> {
for (let j = 0, l = list.length; j < l; j++) {
const newIndex = size++;
newIndexes.push(newIndex);
if (itemStepIdList !== null) {
itemStepIdList[newIndex] = list[j];
}
(ev.entries as any[])[newIndex] = list[j];
// TODO: are these the right flags?
ev._flags[newIndex] = list[j] == null ? FLAG_NULL : NO_FLAGS;

polymorphicPathList[newIndex] =
parentBucket.polymorphicPathList[originalIndex];
Expand Down
2 changes: 1 addition & 1 deletion grafast/grafast/src/engine/OutputPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ export function getChildBucketAndIndex(
"GrafastInternalError<83d0e3cc-7eec-4185-85b4-846540288162>: arrayIndex must be supplied iff outputPlan is an array",
);
}
if (outputPlan && childOutputPlan.layerPlan === bucket.layerPlan) {
if (outputPlan != null && childOutputPlan.layerPlan === bucket.layerPlan) {
// Same layer; straightforward
return [bucket, bucketIndex];
}
Expand Down
4 changes: 2 additions & 2 deletions grafast/grafast/src/engine/executeBucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1276,7 +1276,7 @@ export function bucketToString(this: Bucket) {
export function batchExecutionValue<TData>(
entries: TData[],
_flags: ExecutionEntryFlags[] = arrayOfLength(entries.length, 0),
): ExecutionValue<TData> {
): BatchExecutionValue<TData> {
let cachedStateUnion: ExecutionEntryFlags | null = null;
return {
at: batchEntriesAt,
Expand Down Expand Up @@ -1334,7 +1334,7 @@ function _copyResult(
export function unaryExecutionValue<TData>(
value: TData,
_entryFlags: ExecutionEntryFlags = 0,
): ExecutionValue<TData> {
): UnaryExecutionValue<TData> {
return {
at: unaryAt,
isBatch: false,
Expand Down
3 changes: 3 additions & 0 deletions grafast/grafast/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ import {
LoadStep,
makeDecodeNodeId,
node,
nodeIdFromNode,
NodeStep,
object,
ObjectPlanMeta,
Expand Down Expand Up @@ -401,6 +402,7 @@ export {
newObjectTypeBuilder,
node,
NodeIdCodec,
nodeIdFromNode,
NodeIdHandler,
NodeStep,
noop,
Expand Down Expand Up @@ -525,6 +527,7 @@ exportAsMany("grafast", {
first,
node,
specFromNodeId,
nodeIdFromNode,
polymorphicBranch,
PolymorphicBranchStep,
makeDecodeNodeId,
Expand Down
6 changes: 5 additions & 1 deletion grafast/grafast/src/steps/__flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,12 @@ export class __FlagStep<TData> extends ExecutableStep<TData> {
* Example use case: get user by id, but id is null: no need to fetch the user
* since we know they won't exist.
*/
export function inhibitOnNull<T>($step: ExecutableStep<T>) {
export function inhibitOnNull<T>(
$step: ExecutableStep<T>,
options?: { if?: FlagStepOptions["if"] },
) {
return new __FlagStep<T>($step, {
...options,
acceptFlags: DEFAULT_ACCEPT_FLAGS & ~FLAG_NULL,
});
}
Expand Down
8 changes: 7 additions & 1 deletion grafast/grafast/src/steps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ export {
ListTransformOptions,
ListTransformReduce,
} from "./listTransform.js";
export { makeDecodeNodeId, node, NodeStep, specFromNodeId } from "./node.js";
export {
makeDecodeNodeId,
node,
nodeIdFromNode,
NodeStep,
specFromNodeId,
} from "./node.js";
export { object, ObjectPlanMeta, ObjectStep } from "./object.js";
export { partitionByIndex } from "./partitionByIndex.js";
export {
Expand Down
4 changes: 2 additions & 2 deletions grafast/grafast/src/steps/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,14 +353,14 @@ export class LoadStep<
meta.loadBatchesByLoad.set(this.load, loadBatches);
// Guaranteed by the metaKey to be equivalent for all entries sharing the same `meta`. Note equivalent is not identical; key order may change.
const loadOptions = this.loadOptions!;
setTimeout(() => {
process.nextTick(() => {
// Don't allow adding anything else to the batch
meta.loadBatchesByLoad!.delete(this.load);
executeBatches(loadBatches!, this.load, {
...loadOptions,
unary: unary as TUnarySpec,
});
}, 0);
});
}
return (async () => {
const loadResults = await deferred;
Expand Down
8 changes: 8 additions & 0 deletions grafast/grafast/src/steps/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ export function specFromNodeId(
return handler.getSpec($decoded);
}

export function nodeIdFromNode(
handler: NodeIdHandler<any>,
$node: ExecutableStep,
) {
const specifier = handler.plan($node);
return lambda(specifier, handler.codec.encode);
}

export function makeDecodeNodeId(handlers: NodeIdHandler[]) {
const codecs = [...new Set(handlers.map((h) => h.codec))];

Expand Down
48 changes: 0 additions & 48 deletions grafast/website/examples/users-and-friends/businessLogic.js

This file was deleted.

85 changes: 85 additions & 0 deletions grafast/website/examples/users-and-friends/businessLogic.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Business logic is the same for Grafast and GraphQL
*/

import { db } from "./database.mjs";

const logSql = process.env.LOG_SQL === "1";

const queryAll = (query, parameters) =>
new Promise((resolve, reject) => {
if (logSql) {
console.log({ query, parameters });
}
return db.all(query, parameters, (err, result) =>
err ? reject(err) : resolve(result),
);
});

export async function getUsersByIds(ids, options = {}) {
const columns = options.columns
? [...new Set(["id", ...options.columns])]
: ["*"];
const users = await queryAll(
`select ${columns.join(", ")} from users where id in (${ids
.map(() => `?`)
.join(", ")})`,
ids,
);
return ids.map((id) => users.find((u) => u.id === id));
}

export async function getPostsByIds(ids, options = {}) {
const columns = options.columns
? [...new Set(["id", ...options.columns])]
: ["*"];
const posts = await queryAll(
`select ${columns.join(", ")} from posts where id in (${ids
.map(() => `?`)
.join(", ")})`,
ids,
);
return ids.map((id) => posts.find((u) => u.id === id));
}

export async function getFriendshipsByUserIds(userIds, options = {}) {
const columns = options.columns
? [...new Set(["user_id", ...options.columns])]
: ["*"];
const friendships = await queryAll(
`select ${columns.join(", ")} from friendships where user_id in (${userIds
.map(() => `?`)
.join(", ")})`,
userIds,
);
return userIds.map((userId) =>
friendships.filter((f) => f.user_id === userId),
);
}

export async function getPostsByAuthorIds(rawUserIds, options = {}) {
const userIds = rawUserIds.filter((uid) => uid != null && uid >= 0);
const columns = options.columns
? [...new Set(["author_id", ...options.columns])]
: ["*"];
const posts =
userIds.length > 0
? await queryAll(
`select ${columns.join(", ")} from posts where author_id in (${userIds
.map(() => `?`)
.join(", ")})`,
userIds,
)
: [];
const anonymousPosts =
userIds.length !== rawUserIds.length
? await queryAll(
`select ${columns.join(", ")} from posts where author_id is null`,
)
: [];
return rawUserIds.map((userId) =>
userId == null || userId < 0
? anonymousPosts
: posts.filter((f) => f.author_id === userId),
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const sqlite3 = require("sqlite3");
const db = new sqlite3.Database(":memory:");
import sqlite3 from "sqlite3";
export const db = new sqlite3.Database(":memory:");
const NAMES = [
"Alice",
"Bob",
Expand Down Expand Up @@ -40,6 +40,17 @@ CREATE TABLE friendships (
created_at int not null default CURRENT_TIMESTAMP,
updated_at int
);
`,
);
db.run(
`\
CREATE TABLE posts (
id int primary key,
author_id int,
content text,
created_at int not null default CURRENT_TIMESTAMP,
updated_at int
);
`,
);

Expand Down Expand Up @@ -74,8 +85,25 @@ CREATE TABLE friendships (
}
stmt.finalize();
}

{
const stmt = db.prepare(
"INSERT INTO posts (id, author_id, content) VALUES (?, ?, ?)",
);
let id = 0;
for (let nameIndex = 0; nameIndex < NAMES.length; nameIndex++) {
// A stable list of friendIds
const friendIndexes = NAMES.map((_, idx) => idx).filter(
(idx) => idx % (nameIndex + 1) === 0,
);
for (const friendIndex of friendIndexes) {
stmt.run(++id, nameIndex + 1, `I'm friends with ${NAMES[friendIndex]}`);
}
}
stmt.run(++id, null, `Have you heard about updog?`);
stmt.run(++id, null, `Sillypeoplesaywhat.`);
stmt.finalize();
}
});

// db.close();

exports.db = db;
13 changes: 0 additions & 13 deletions grafast/website/examples/users-and-friends/dataloaders.js

This file was deleted.

Loading

0 comments on commit b901cf0

Please sign in to comment.