Noah/refactor filtering - #287
Conversation
approach, which resulted in any type intermediate values, ruining everything.
| 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 |
There was a problem hiding this comment.
I'd probably read this code in editor, rather than diff-view - its essentially a re-write of most of the fun parts.
- gone are the complicated template-string style types.
- replaced are shorter, templated builder-style functions, which infer their types from the parameters, which are usually very short strings.
…titute/vis into noah/refactor-filtering
lanesawyer
left a comment
There was a problem hiding this comment.
Handful of comments, looks to be working well, the demo is still functional!
|
|
||
| ```ts | ||
| .given({ | ||
| const { table, clause, select, all } = given({ |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
Demo still works and shows a table of read data!
| ); | ||
| return Q.shader; | ||
| }, | ||
| build: (device: GPUDevice, label: string) => { |
There was a problem hiding this comment.
build and buildIndex are very similar, maybe extract a helper function? Or do we expect them to diverge more substantially over time?
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
A TODO worth solving now?
| 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), | ||
| ], | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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),
],
}),
);| for (let i = 0; i < sets.length; i++) { | ||
| const s = sets[i]!; | ||
| const bg1 = bindings[i]!; | ||
| // console.log('running filter-->',s.results.label) |
There was a problem hiding this comment.
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... |
There was a problem hiding this comment.
mapValues does have a generic parameter but I'm having trouble making it happy...
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
Oooh maybe we move to lodash-es here too soon, didn't realize we had only done that in one repo so far.
There was a problem hiding this comment.
yeah I'd love to - ye olde lodash imports break the build regularly unless I do absolutely wacky stuff.
There was a problem hiding this comment.
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!
field - its more readable even!
for query building helpers
Jarbuckle
left a comment
There was a problem hiding this comment.
Just a couple things, looking good overall!
| ${outputStructDecl} | ||
| ${decl} |
There was a problem hiding this comment.
Looks like this got reverted somehow 😅
There was a problem hiding this comment.
fixed, sorry about that...
| inputs.push(buf); | ||
| return buf; | ||
| }) | ||
| ) as BufferTables<Ts>; // TS cant tell, mapValues I think does erase the info... |
There was a problem hiding this comment.
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. 😞
| 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'); | ||
| } | ||
| }) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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!
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
PR Checklist
main?