Skip to content

Noah/refactor filtering - #287

Merged
froyo-np merged 17 commits into
mainfrom
noah/refactor-filtering
Aug 17, 2026
Merged

Noah/refactor filtering#287
froyo-np merged 17 commits into
mainfrom
noah/refactor-filtering

Conversation

@froyo-np

Copy link
Copy Markdown
Collaborator

What

the initial implementation of filtering did not support some features (like referring to the .x or .y value of a vector column) that were critical for the actual filters we need in our use case. Making this all work required a bit of a rewrite, but its much more capable now, and the "andOpen" style syntax, which was confusing, is now replaced with something that is still a bit confusing, but better and you get more power from it.

How

use a builder-style expression system, rather than very fragile typescript template-string types.

Screenshots

This section is optional if there are no visible changes

  • If possible add screenshots of the visible additions in the UI.
  • If there are changes in the UI, add Before and After Screenshots for quick overview.
  • If there was a Figma design, add a link to that here as well.
  • Hint : Drag and Drop any images you want to add to the PR. Also you can create a gif of an interactive version and add that!

PR Checklist

  • Is your PR title following our conventional commit naming recommendations?
  • Have you filled in the PR Description Template?
  • Is your branch up to date with the latest in main?
  • Do the CI checks pass successfully?
  • Have you smoke tested the example applications?
  • Did you check that the changes meet accessibility standards?
  • Have you tested the application on these browsers?
    • Chrome (Fully supported)
    • Firefox (Major bug fixes supported)
    • Safari (Major bug fixes supported)

@froyo-np
froyo-np requested a review from a team as a code owner August 13, 2026 17:37
@froyo-np
froyo-np requested review from Jarbuckle, TheMooseman and lanesawyer and removed request for lanesawyer August 13, 2026 17:37
export type VarName = `${letter}${string}`;
// type Var<T extends VarName> = `$${T}`
// I want a type error if the op type is a vOp and either operand is not...
export type PredicateExpr<T extends ITable, K extends keyof T, Param extends VarName> = K extends string

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'd probably read this code in editor, rather than diff-view - its essentially a re-write of most of the fun parts.

  1. gone are the complicated template-string style types.
  2. replaced are shorter, templated builder-style functions, which infer their types from the parameters, which are usually very short strings.

@lanesawyer lanesawyer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Handful of comments, looks to be working well, the demo is still functional!


```ts
.given({
const { table, clause, select, all } = given({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah, I like this approach a bit better. Definitely more verbose, but not detrimentally so.

: Params & { [k in Param]: TsType<Ts[O][F]> }
>(this.ctx, [...this.predicates, { OP: 'and', pred }]);
/* oxlint-disable no-console, typescript/no-explicit-any*/
const entries = <T extends {}>(r: T): ReadonlyArray<[keyof T, T[keyof T]]> => Object.entries(r) as any;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hm, I don't love the as any force casting here and in gen.ts but I'm not seeing a really straightforward other option that I've liked as I've played aroudn with it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yeah - the way it works out is essentially Object.entries(r) as any as ReadonlyArray<[keyof T, T[keyof T]]>
mostly this was me struggling with both lodash entries and Object.entries types given my very fragile type stuff going on here - I'll try and get rid of the cast, but to be fair, its pretty contained - the any cant escape!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ok - this was noah having Typescript fever - I've removed the nonsense here and now lots of stuff lower down no longer needs silly casting.

.build(device, 'testing');
const { all, any, column, table, select, clause } = given(tableLayout).from('edges');

const filter = select('$index')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Demo still works and shows a table of read data!

);
return Q.shader;
},
build: (device: GPUDevice, label: string) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

build and buildIndex are very similar, maybe extract a helper function? Or do we expect them to diverge more substantially over time?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

they differ only in that one returns a function which requires an elements buffer to be passed, full of indexes. I think I can wrestle the typechecker... these are only separate so that the inferrence of that requirement works out - I'll give it a shot.

// ok now get the results and compare them!
Promise.all([resolve.mapAsync(GPUMapMode.READ), resolveCount.mapAsync(GPUMapMode.READ)])
.then(() => {
const count = new Uint32Array(resolveCount.getMappedRange());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This variable is unused. Should it be used? Otherwise delete!

const runner = (args: Indexed extends true ? RunIndexedFilterArgs<Ts> : RunFilterArgs<Ts>) => {
const { enc, parameters, sets, timestampWrites } = args;
// here, we zero out the result counters
// TODO - consider not doing this - if we didnt do that:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A TODO worth solving now?

Comment on lines +245 to +266
const bindings = indexed
? (args as RunIndexedFilterArgs<Ts>).sets.map((s, i) => {
return device.createBindGroup({
layout: pipe.pipeline.getBindGroupLayout(1),
entries: [
{ binding: 0, resource: s.resultCounter },
{ binding: 1, resource: s.results },
{ binding: 2, resource: s.elements },
...mapTablesToBindings(s.tables, safeLookups),
],
});
})
: args.sets.map((s, i) => {
return device.createBindGroup({
layout: pipe.pipeline.getBindGroupLayout(1),
entries: [
{ binding: 0, resource: s.resultCounter },
{ binding: 1, resource: s.results },
...mapTablesToBindings(s.tables, safeLookups),
],
});
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could simplify this a bit, the only difference is the { binding: 2, resource: s.elements }, bit. Don't know that I love having the indexed && "elements" in s check though, could obfuscate a little bit what's happening, since folks might not look deeply into entries and not understand the indexed versus not indexed take.

Up to you, just saw a little bit of code duplication that could be avoided in a few different ways!

                const bindings = args.sets.map((s) =>
                    device.createBindGroup({
                        layout: pipe.pipeline.getBindGroupLayout(1),
                        entries: [
                            { binding: 0, resource: s.resultCounter },
                            { binding: 1, resource: s.results },
                            ...(indexed && "elements" in s ? [{ binding: 2, resource: s.elements }] : []),
                            ...mapTablesToBindings(s.tables, safeLookups),
                        ],
                    }),
                );

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I like it!

for (let i = 0; i < sets.length; i++) {
const s = sets[i]!;
const bg1 = bindings[i]!;
// console.log('running filter-->',s.results.label)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can remove, or potentially do a logger.debug here so we could turn these on later if we choose.

I don't think our logger gets stripped from the build though so no-ops would be annoying for performance... Maybe something for us to look into later so we can feel more free to sprinkle debug logs around.

inputs.push(buf);
return buf;
})
) as BufferTables<Ts>; // TS cant tell, mapValues I think does erase the info...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

mapValues does have a generic parameter but I'm having trouble making it happy...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, also dug into this one, not finding a working variant of mapValues that solves this problem -- all of them erase key-typing in one way or another. 😞

ArrayBufferTables,
} from './types';
import * as wgh from 'webgpu-utils';
import * as lo from 'lodash';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oooh maybe we move to lodash-es here too soon, didn't realize we had only done that in one repo so far.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yeah I'd love to - ye olde lodash imports break the build regularly unless I do absolutely wacky stuff.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Replacement PR here! #288

We can merge yours first and I can come clean up the remainder later, or you can review and merge that and start using in this PR. Either way!

@Jarbuckle Jarbuckle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a couple things, looking good overall!

Comment on lines +176 to +26
${outputStructDecl}
${decl}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like this got reverted somehow 😅

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed, sorry about that...

inputs.push(buf);
return buf;
})
) as BufferTables<Ts>; // TS cant tell, mapValues I think does erase the info...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, also dug into this one, not finding a working variant of mapValues that solves this problem -- all of them erase key-typing in one way or another. 😞

Comment on lines +370 to +379
for (let i = 0; i < expected.buffer.byteLength; i++) {
if (dv.getUint8(i) !== ex.getUint8(i)) {
logger.error('filter validation failed at byte: ', i);
failBytes += 1;
}
}
if (failBytes === 0) {
logger.info('validation success');
}
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How many comparisons is this doing, on average? 🤔 And do we need to know the number of bytes that failed? Also, genuinely curious if we want to keep going if we get failures? Would that be a filter algorithm issue, a hardware issue, both, neither?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

the idea of this is a smoke-test. it will certainly fail if you ask it to filter more than 64 rows of data, so at worst, this will fail after 512 checks. I've re-written this to return a copy of the result buffer in the failing case, and it now stops at the first failed byte.

in the validate, stop validating at the first failed byte, return the
copy for the user to determine what went wrong

@lanesawyer lanesawyer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Changes make sesne, thanks for the test, and the demo still runs. I think any other pain points can be ironed out again as we hit them while making further progress on Connectomics!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Simple but effective at proving the output looks as expected! Thanks!

ArrayBufferTables,
} from './types';
import * as wgh from 'webgpu-utils';
import * as lo from 'lodash';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Replacement PR here! #288

We can merge yours first and I can come clean up the remainder later, or you can review and merge that and start using in this PR. Either way!

@froyo-np
froyo-np merged commit f0e45cb into main Aug 17, 2026
5 checks passed
@froyo-np
froyo-np deleted the noah/refactor-filtering branch August 17, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants