Skip to content
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

Strict DFS traversal #359

Closed
guanzo opened this issue Sep 13, 2023 · 8 comments · Fixed by #361
Closed

Strict DFS traversal #359

guanzo opened this issue Sep 13, 2023 · 8 comments · Fixed by #361
Labels
need/triage Needs initial labeling and prioritization

Comments

@guanzo
Copy link

guanzo commented Sep 13, 2023

The Trustless Gateway spec is being productionized by Saturn, Daghaus and possibly others. It's important for DAG traversal clients to be able to consume CARs with blocks in DFS order as this is optimal for streaming and incremental verification. While DFS isn't required by the spec, it seems to be the preferred traversal order as it's the only explicitly mentioned order, the other being "unknown".

This is the section that performs BFS, there may be more though.

for (let i = 0; i < node.Links.length; i++) {
const childLink = node.Links[i]
const childStart = streamPosition // inclusive
const childEnd = childStart + file.blockSizes[i] // exclusive
if ((start >= childStart && start < childEnd) || // child has offset byte
(end >= childStart && end <= childEnd) || // child has end byte
(start < childStart && end > childEnd)) { // child is between offset and end bytes
childOps.push({
link: childLink,
blockStart: streamPosition
})
}
streamPosition = childEnd
if (streamPosition > end) {
break
}
}
await pipe(
childOps,
(source) => map(source, (op) => {
return async () => {
const block = await blockstore.get(op.link.Hash, options)
return {
...op,
block
}
}
}),
(source) => parallel(source, {
ordered: true
}),
async (source) => {
for await (const { link, block, blockStart } of source) {
let child: dagPb.PBNode | Uint8Array
switch (link.Hash.code) {
case dagPb.code:
child = dagPb.decode(block)
break
case raw.code:
child = block
break
default:
queue.end(errCode(new Error(`Unsupported codec: ${link.Hash.code}`), 'ERR_NOT_UNIXFS'))
return
}
// create a queue for this child - we use a queue instead of recursion
// to avoid overflowing the stack
const childQueue = new PQueue({
concurrency: 1
})
// if any of the child jobs error, end the read queue with the error
childQueue.on('error', error => {
queue.end(error)
})
// if the job rejects the 'error' event will be emitted on the child queue
void childQueue.add(async () => {
options.onProgress?.(new CustomProgressEvent<ExportWalk>('unixfs:exporter:walk:file', {
cid: link.Hash
}))
await walkDAG(blockstore, child, queue, blockStart, start, end, options)
})
// wait for this child to complete before moving on to the next
await childQueue.onIdle()
}
}
)

@guanzo guanzo added the need/triage Needs initial labeling and prioritization label Sep 13, 2023
@rvagg
Copy link
Member

rvagg commented Sep 13, 2023

I believe that section is doing DFS. See the bottom most part of it. Just collecting links but then serially walking those links and resolving what's below them.

https://github.com/web3-storage/freeway I believe is making use of this same code to produce DFS order for us. I think originally they had some BFS code internally that they had to unwind but from what I understand the unixfs internals aren't the problem area.

@guanzo
Copy link
Author

guanzo commented Sep 14, 2023

OK I really hope I'm not misunderstanding something.

Just collecting links

The issue is, blockstore.get is called for each link, before resolving each links children.

const block = await blockstore.get(op.link.Hash, options)

Incremental verification fails on this CID that's fetched from lassie: bafybeigrbpmdsqaift2qwzy32bjyywkx6nzmn66pjeoaie6egpbktykc6e

If I log every blockstore.get call, js-ipfs-unixfs will print

get CID(bafybeigrbpmdsqaift2qwzy32bjyywkx6nzmn66pjeoaie6egpbktykc6e)
get CID(bafybeiea2qi6j3jgqo5fu3lsski2iluovy6t7q5izs4vorjkfnkjfio5sa)
get CID(bafybeifsqgsqwjceoa6k5aszqqnwolcq65exbbpmem5wieynrid62tg3lm)
get CID(bafkreigops5bbgf35ah22lurd45kp7yzlpditmsfro3syxwwgovjr54fqi)
get CID(bafkreiez3s2vsggbjnzue3mczfrcmywpoy3xwnvwfesew34m4tzqzo2fs4)
...

If I use dagula, it prints the correct DFS order

get CID(bafybeigrbpmdsqaift2qwzy32bjyywkx6nzmn66pjeoaie6egpbktykc6e)
get CID(bafybeiea2qi6j3jgqo5fu3lsski2iluovy6t7q5izs4vorjkfnkjfio5sa)
get CID(bafkreigops5bbgf35ah22lurd45kp7yzlpditmsfro3syxwwgovjr54fqi)
get CID(bafkreiez3s2vsggbjnzue3mczfrcmywpoy3xwnvwfesew34m4tzqzo2fs4)
get CID(bafkreig2mplsficluccozctgebykmjcrbvb6ida5zccz2ww3zoionoeniy)
...

The DAG looks like

$ ipfs dag get bafybeigrbpmdsqaift2qwzy32bjyywkx6nzmn66pjeoaie6egpbktykc6e | jq
{
  "Data": {
    "/": {
      "bytes": "CAIYmMXIGiCAgOAVIJjF6AQ"
    }
  },
  "Links": [
    {
      "Hash": {
        "/": "bafybeiea2qi6j3jgqo5fu3lsski2iluovy6t7q5izs4vorjkfnkjfio5sa"
      },
      "Name": "",
      "Tsize": 45621766
    },
    {
      "Hash": {
        "/": "bafybeifsqgsqwjceoa6k5aszqqnwolcq65exbbpmem5wieynrid62tg3lm"
      },
      "Name": "",
      "Tsize": 10103360
    }
  ]
}

You can see that js-ipfs-unixfs traverses ...3lm before ...5sa's children.

Does calling blockstore.get count as a traversal?

EDIT: Actually it's not totally BFS, it's just the first layer of children being visited before any of the sub children

@rvagg
Copy link
Member

rvagg commented Sep 14, 2023

mmm, you're probably right about that! not sure about the async iteration going on there but probably doing all those blockstore fetches in parallel first; maybe worth finding out how freeway is using this code to do it? or perhaps it's just using js-unixfs for this side of it instead

@rvagg
Copy link
Member

rvagg commented Sep 14, 2023

Or ... perhaps we haven't noticed traversal order problems with freeway because large file fetching is more rare, maybe I need to go back and look at the error logs. You have to have a pretty big file to end up with multiple layers.

@guanzo
Copy link
Author

guanzo commented Sep 14, 2023

Yep I've been using this lib for a while without issue. It was only until I implemented incremental verification and tried rendering a 50MB image that I ran into this problem.

I made a hasty fix here due to time constraints: filecoin-saturn@ee5a574

@achingbrain
Copy link
Member

The traversal is DFS, that is, the leaf node data is emitted depth-first, but internally the exporter applies an optimisation to load all the children in the DAG as soon as they are encountered.

The links are processed in-order and as soon as they are ready rather than waiting for the last to load before the first is processed.

This speeds up the case for when the blockstore has to go to the network or it uses some other slow retrieval method, as it has a headstart for when you do actually need a sibling block, but is why it's called in an order that looks like it's doing BFS.

A quick fix might be to expose a config option for the it-parallel concurrency parameter and pass it in here - if you set it to 1 then the blocks should be loaded from the blockstore in an order consistent with DFS.

@alanshaw
Copy link
Member

@rvagg freeway does not use the unixfs exporter to create CARs with DFS block ordering.

@guanzo guanzo changed the title Switch DAG traversal from breadth first to depth first Strict DFS traversal Sep 18, 2023
@guanzo
Copy link
Author

guanzo commented Sep 18, 2023

Ok that makes sense.

For context, Saturn retrieval clients expect CAR file blocks to be in DFS order, and it's implemented by having the traversal client (in this case js-ipfs-unixfs) ask the blockstore for blocks in the expected order. There isn't really an easy workaround in userland since the blockstore lacks any traversal context, so a workaround would be appreciated 🙏 .

achingbrain added a commit that referenced this issue Sep 20, 2023
By default we attempt to load all sibilings in a given layer of a
DAG at once to allow slow/async loading routines extra time to fetch
data before it is needed.

Some blockstores (e.g. CAR files) require the exporter to only request
the next sequential CID in a DAG.

Add a `blockReadConcurrency` option (named similarly to the importer's
`blockWriteConcurrency` option) to control this behaviour.

Fixes #359
achingbrain added a commit that referenced this issue Jan 19, 2024
By default we attempt to load all siblings in a given layer of a
DAG at once to allow slow/async loading routines extra time to fetch
data before it is needed.

Some blockstores (e.g. CAR files) require the exporter to only request
the next sequential CID in a DAG.

Add a `blockReadConcurrency` option (named similarly to the importer's
`blockWriteConcurrency` option) to control this behaviour.

Fixes #359

---------

Co-authored-by: Rod Vagg <rod@vagg.org>
github-actions bot pushed a commit that referenced this issue Jan 19, 2024
## ipfs-unixfs-exporter [13.4.0](ipfs-unixfs-exporter-13.3.1...ipfs-unixfs-exporter-13.4.0) (2024-01-19)

### Features

* add blockReadConcurrency option to exporter ([#361](#361)) ([295077e](295077e)), closes [#359](#359)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
need/triage Needs initial labeling and prioritization
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants