Repository: deno
Issue: Writing JS strings to resources, Deno.encode and Deno.encodeInto
Labels: perf

## Issue

I was doing some investigation into network perf using Deno FFI and stdio perf in general and came up against a performance roadblock which is relevant to anywhere we are writing JS strings to ops resources using send or write syscalls and the like.

If you look at the benchmark script [here](https://github.com/denoland/performance-team/blob/main/benchmarks/io/02-strings/bench.js#L34) you can see the following scenarios benchmarked. All scenarios write to /dev/null using FFI fast call.

## Benchmarks

- **static_buffer**: writing a pre-existing buffer with the string pre-encoded - nothing should be faster than this afaik
- **encode_dynamic_string**: writing a dynamically generated string encoded into a new Uint8Array using ```Deno.core.encode```
- **encode_static_string**: writing a static string encoded into a new Uint8Array using ```Deno.core.encode```
- **encode_into_dynamic_string**: writing a dynamically generated string encoded into an existing Uint8Array using ```Deno.core.ops.op_encoding_encode_into```
- **encode_into_static_string**: writing a static string encoded into an existing Uint8Array using ```Deno.core.ops.op_encoding_encode_into```

## Results
![results](https://user-images.githubusercontent.com/159073/190502364-d8cf38c1-6d61-4daa-b642-54f03c37c616.png)

## Flamegraphs

Flamegraphs to compare the three static string scenarios are here which show clearly the overhead involved in both ```Deno.core.encode``` and ```Deno.core.ops.op_encoding_encode_into``` compared to the baseline of writing a pre-encoded buffer and using Deno.writeSync, which seems to be fastest option currently exposed to userland.

```
const tests = {
  static_buffer_writesync: () => Deno.writeSync(rid, payload),
  static_buffer_ffi: () => fs.write(fd, payload, payload.length),
  encode_dynamic_string: () => {
    const payload = encode(`${TEXT}${byteLength(str)}${CRLF2}${str}`)
    fs.write(fd, payload, payload.length)
  },
  encode_static_string: () => {
    const payload = encode(resp)
    fs.write(fd, payload, payload.length)
  },
  encode_into_dynamic_string: () => {
    const { written } = ops.op_encoding_encode_into(`${TEXT}${byteLength(str)}${CRLF2}${str}`, buf)
    fs.write(fd, buf, written)
  },
  encode_into_static_string: () => {
    const { written } = ops.op_encoding_encode_into(resp, buf)
    fs.write(fd, buf, written)
  }
}
```
### static_buffer_writesync
![static_buffer_writesync](https://user-images.githubusercontent.com/159073/190502916-5d7aa789-9068-46e3-b371-19d46f32d411.svg)

### static_buffer_ffi
![static_buffer_ffi](https://user-images.githubusercontent.com/159073/190502959-37a3d899-6d9e-4b35-9c21-1adc6d5e91c2.svg)

### encode_static_string
![encode_static_string](https://user-images.githubusercontent.com/159073/190503003-60d395c8-28ab-4928-948e-a36fae971858.svg)

### encode_into_static_string
![encode_into_static_string](https://user-images.githubusercontent.com/159073/190503051-d0f611f9-27f1-49e5-bd7d-9383732ed616.svg)

## Discussion

From the benchmarks we can see:
- calling ```Deno.core.encode``` on the static string in question causes a **~58%** drop in throughput and takes **2.3x** longer
- calling ```Deno.core.ops.op_encoding_encode_into``` causes a **~64%** drop in throughput and takes **3.27x** longer
- JS String concatentation adds **~15%** overhead

From the flamegraphs we can see:
- with a static buffer, the program spends **88%** of time in ```__libc_write```
- with ```Deno.core.encode``` the program spends **35%** in ```__libc_write```
- with ```Deno.core.ops.op_encoding_encode_into``` the program spends **28%** in ```__libc_write```

There are a few of issues I think this raises:
- ```Deno.core.encode``` is expensive because it does a lot of work - it allocates a BackingStore, creates an ArrayBuffer and wraps it in a Uint8Array and returns that to JS. This is per spec so not much we can do about it other than to avoid using it where we don't need to.
-  ```Deno.core.ops.op_encoding_encode_into``` **should** be faster than Deno.core.encode as it is working with a pre-existing buffer and only needs to do a memcpy from the string into the buffer. The overhead here is likely down to it being a native op and being wrapped in serde_v8.
- We need a method for writing a JS string directly to an ops resource rid. I came across the same issue with just-js and had to do something like this. This is significantly quicker than first encoding into a buffer and then writing the buffer as far as i can remember.

```C++
  String::Utf8Value str(isolate, args[1]);
  args.GetReturnValue().Set(Integer::New(isolate, write(fd, 
    *str, str.length())));
```

## Planned Actions

- add an [op-v8] api call ```Deno.core.encode_into (u8, String)``` into ops_builtin_v8.rs which does encode_into without serde_v8.
- add a ```Deno.core.write_utf8 (rid, string)``` api call to [ops_builtin_v8.rs](https://github.com/denoland/deno/blob/main/core/ops_builtin_v8.rs) which will take a copy of the utf8 bytes inside the v8 string passed to it and write them to the resource identified by rid, as in the C++ snippet above.

The disadvantage of the second approach is, afaik, it will not be optimised as a fast call because fast call api has no string support yet. But it would be worth knowing if it is faster than the other methods above even without the fast call so I propose doing it as a draft PR and see if it improves perf. It should be pretty easy to implement.