Skip to content

Commit

Permalink
doc: add performance tip for napi
Browse files Browse the repository at this point in the history
  • Loading branch information
HerringtonDarkholme committed Jun 24, 2024
1 parent 0a9a3a5 commit c5e7605
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 1 deletion.
2 changes: 2 additions & 0 deletions website/guide/api-usage/js-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,5 @@ const edit = node.replace('console.error($A)')
const newSource = node.commitEdits([edit])
// "console.error('hello world')"
```

See also [ast-grep#1172](https://github.com/ast-grep/ast-grep/issues/1172)
55 changes: 54 additions & 1 deletion website/guide/api-usage/performance-tip.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,59 @@
# Performance Tip for napi usage

## `findInFiles`
Using `napi` to parse code and search for nodes [isn't always faster](https://medium.com/@hchan_nvim/benchmark-typescript-parsers-demystify-rust-tooling-performance-025ebfd391a3) than pure JavaScript implementations.

There are a lot of tricks to improve performance when using `napi`. The mantra is to _reduce FFI (Foreign Function Interface) calls between Rust and JavaScript_, and to _take advantage of parallel computing_.

## Prefer `parseAsync` over `parse`

`parseAsync` can take advantage of NodeJs' libuv thread pool to parse code in parallel threads. This can be faster than the sync version `parse` when handling a lot of code.

```ts
import { js } from '@ast-grep/napi';
// only one thread parsing
const root = js.parse('console.log("hello world")')
// better, can use multiple threads
const root = await js.parseAsync('console.log("hello world")')
```

This is especially useful when you are using ast-grep in bundlers where the main thread is busy with other CPU intensive tasks.

## Prefer `findAll` over manual traversal

One way to find all nodes that match a rule is to traverse the syntax tree manually and check each node against the rule. This is slow because it requires a lot of FFI calls between Rust and JavaScript during the traversal.

For example, the following code snippet finds all `member_expression` nodes in the syntax tree. Unfortunately, there are as many FFI calls as the tree node number in the recursion.

```ts
const root = sgroot.root()
function findMemberExpression(node: SgNode): SgNode[] {
let ret: SgNode[] = []
// `node.kind()` is a FFI call
if (node.kind() === 'member_expression') {
ret.push(node)
}
// `node.children()` is a FFI call
for (let child of node.children()) {
// recursion makes more FFI calls
ret = ret.concat(findMemberExpression(child))
}
return ret
}
const nodes = findMemberExpression(root)
```

The equivalent code using `findAll` is much faster:

```ts
const root = sgroot.root()
// only call FFI `findAll` once
const nodes = root.findAll({kind: 'member_expression'})
```

> _One [success](https://x.com/hd_nvim/status/1767971906786128316) [story](https://x.com/sonofmagic95/status/1768433654404104555) on Twitter, as an example._

## Prefer `findInFiles` when possible

If you have a lot of files to parse and want to maximize your programs' performance, ast-grep's language object provides a `findInFiles` function that parses multiple files and searches relevant nodes in parallel Rust threads.

Expand Down

0 comments on commit c5e7605

Please sign in to comment.