Repository: denoland/deno
Pull request: perf(buffer): optimize Buffer.subarray and Buffer.prototype.utf8Slice

Benchmark (what drove this):

```js
import postgres from "npm:postgres";
const sql = postgres();

// Create the table if it doesn't exist
await sql`
    CREATE TABLE IF NOT EXISTS "silly_postgres_bench" (
      id SERIAL PRIMARY KEY,
      first_name TEXT NOT NULL,
      last_name TEXT NOT NULL,
      email TEXT NOT NULL UNIQUE,
      dob TEXT NOT NULL
    )
  `;

// Check if users already exist
const existingUsers =
  await sql`SELECT COUNT(*) as count FROM "silly_postgres_bench"`;

if (+(existingUsers?.[0]?.count ?? existingUsers?.count) < 100) {
  // Generate 100 users if none exist
  const users = Array.from({ length: 100 }, (_, i) => ({
    first_name: `FirstName${i}`,
    last_name: `LastName${i}`,
    email: `user${i}@example.com`,
    dob: new Date(
      1970 + Math.floor(Math.random() * 30),
      Math.floor(Math.random() * 12),
      Math.floor(Math.random() * 28),
    )
      .toISOString()
      .split("T")[0],
  }));

  // Insert all users
  await sql`
      INSERT INTO silly_postgres_bench ${sql(users)}
    `;
}

console.time("deno");
const promises = [];
for (let i = 0; i < 100_000; i++) {
  promises.push(sql`SELECT * FROM "silly_postgres_bench" LIMIT 100`);
  if (i % 100 === 0 && promises.length > 1) {
    await Promise.all(promises);
    promises.length = 0;
  }
}
await Promise.all(promises);
console.timeEnd("deno");
```

Results:

main:

```
deno: 11981ms
```

This PR:

```
deno: 5720ms
```

Changed files:
filename: ext/node/lib.rs
patch: @@ -239,6 +239,7 @@ deno_core::extension!(deno_node,
     ops::buffer::op_is_ascii,
     ops::buffer::op_is_utf8,
     ops::buffer::op_transcode,
+    ops::buffer::op_node_decode_utf8,
     ops::crypto::op_node_check_prime_async,
     ops::crypto::op_node_check_prime_bytes_async,
     ops::crypto::op_node_check_prime_bytes,

filename: ext/node/ops/buffer.rs
patch: @@ -1,6 +1,7 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 
 use deno_core::op2;
+use deno_core::v8;
 use deno_error::JsErrorBox;
 
 #[op2(fast)]
@@ -119,3 +120,73 @@ fn utf8_to_ascii(source: &[u8]) -> Vec<u8> {
   }
   ascii_bytes
 }
+
+#[op2]
+pub fn op_node_decode_utf8<'a>(
+  scope: &mut v8::HandleScope<'a>,
+  buf: v8::Local<v8::ArrayBufferView>,
+  start: v8::Local<v8::Value>,
+  end: v8::Local<v8::Value>,
+) -> Result<v8::Local<'a, v8::String>, JsErrorBox> {
+  let buf = buf.get_contents(&mut [0; v8::TYPED_ARRAY_MAX_SIZE_IN_HEAP]);
+
+  let start =
+    parse_array_index(scope, start, 0).map_err(JsErrorBox::from_err)?;
+  let mut end =
+    parse_array_index(scope, end, buf.len()).map_err(JsErrorBox::from_err)?;
+
+  if end < start {
+    end = start;
+  }
+
+  if end > buf.len() {
+    return Err(JsErrorBox::from_err(BufferError::OutOfRange));
+  }
+
+  let buffer = &buf[start..end];
+
+  if buffer.len() <= 256 && buffer.is_ascii() {
+    v8::String::new_from_one_byte(scope, buffer, v8::NewStringType::Normal)
+      .ok_or_else(|| JsErrorBox::from_err(BufferError::StringTooLong))
+  } else {
+    v8::String::new_from_utf8(scope, buffer, v8::NewStringType::Normal)
+      .ok_or_else(|| JsErrorBox::from_err(BufferError::StringTooLong))
+  }
+}
+
+#[derive(Debug, thiserror::Error, deno_error::JsError)]
+enum BufferError {
+  #[error("String too long")]
+  #[class(generic)]
+  #[property("code" = "ERR_STRING_TOO_LONG")]
+  StringTooLong,
+  #[error("Invalid type")]
+  #[class(generic)]
+  InvalidType,
+  #[error("Index out of range")]
+  #[class(range)]
+  #[property("code" = "ERR_OUT_OF_RANGE")]
+  OutOfRange,
+}
+
+#[inline(always)]
+fn parse_array_index(
+  scope: &mut v8::HandleScope,
+  arg: v8::Local<v8::Value>,
+  default: usize,
+) -> Result<usize, BufferError> {
+  if arg.is_undefined() {
+    return Ok(default);
+  }
+
+  let Some(arg) = arg.integer_value(scope) else {
+    return Err(BufferError::InvalidType);
+  };
+  if arg < 0 {
+    return Err(BufferError::OutOfRange);
+  }
+  if arg > isize::MAX as i64 {
+    return Err(BufferError::OutOfRange);
+  }
+  Ok(arg as usize)
+}

filename: ext/node/polyfills/internal/buffer.mjs
patch: @@ -22,6 +22,7 @@ const {
   Float64Array,
   MathFloor,
   MathMin,
+  MathTrunc,
   Number,
   NumberIsInteger,
   NumberIsNaN,
@@ -45,7 +46,6 @@ const {
   SymbolFor,
   SymbolSpecies,
   SymbolToPrimitive,
-  TypeError,
   TypeErrorPrototype,
   TypedArrayPrototypeCopyWithin,
   TypedArrayPrototypeFill,
@@ -63,6 +63,7 @@ import {
   op_is_ascii,
   op_is_utf8,
   op_node_call_is_from_dependency,
+  op_node_decode_utf8,
   op_transcode,
 } from "ext:core/ops";
 
@@ -665,9 +666,21 @@ Buffer.prototype.swap64 = function swap64() {
   return this;
 };
 
+function decodeUtf8(buffer, start, end) {
+  return op_node_decode_utf8(
+    buffer,
+    start,
+    end,
+  );
+}
+
 Buffer.prototype.toString = function toString(encoding, start, end) {
   if (arguments.length === 0) {
-    return this.utf8Slice(0, this.length);
+    return decodeUtf8(
+      this,
+      0,
+      this.length,
+    );
   }
 
   const len = this.length;
@@ -678,6 +691,9 @@ Buffer.prototype.toString = function toString(encoding, start, end) {
     return "";
   } else {
     start |= 0;
+    if (start <= 0) {
+      start = 0;
+    }
   }
 
   if (end === undefined || end > len) {
@@ -690,8 +706,12 @@ Buffer.prototype.toString = function toString(encoding, start, end) {
     return "";
   }
 
-  if (encoding === undefined) {
-    return this.utf8Slice(start, end);
+  if (encoding === undefined || encoding === "utf8") {
+    return decodeUtf8(
+      this,
+      start,
+      end,
+    );
   }
 
   const ops = getEncodingOps(encoding);
@@ -1041,9 +1061,15 @@ Buffer.prototype.ucs2Write = function ucs2Write(string, offset, length) {
   );
 };
 
-Buffer.prototype.utf8Slice = function utf8Slice(string, offset, length) {
-  return _utf8Slice(this, string, offset, length);
-};
+Buffer.prototype.utf8Slice = utf8Slice;
+
+function utf8Slice(start, end) {
+  return decodeUtf8(
+    this,
+    start,
+    end,
+  );
+}
 
 Buffer.prototype.utf8Write = function utf8Write(string, offset, length) {
   // deno-lint-ignore prefer-primordials
@@ -1192,6 +1218,35 @@ function _hexSlice(buf, start, end) {
   return out;
 }
 
+function adjustOffset(offset, length) {
+  // Use Math.trunc() to convert offset to an integer value that can be larger
+  // than an Int32. Hence, don't use offset | 0 or similar techniques.
+  offset = MathTrunc(offset);
+  if (offset === 0) {
+    return 0;
+  }
+  if (offset < 0) {
+    offset += length;
+    return offset > 0 ? offset : 0;
+  }
+  if (offset < length) {
+    return offset;
+  }
+  return NumberIsNaN(offset) ? 0 : length;
+}
+
+Buffer.prototype.subarray = function subarray(start, end) {
+  const srcLength = this.length;
+  start = adjustOffset(start, srcLength);
+  end = end !== undefined ? adjustOffset(end, srcLength) : srcLength;
+  const newLength = end > start ? end - start : 0;
+  return new FastBuffer(
+    TypedArrayPrototypeGetBuffer(this),
+    TypedArrayPrototypeGetByteOffset(this) + start,
+    newLength,
+  );
+};
+
 Buffer.prototype.slice = function slice(start, end) {
   return this.subarray(start, end);
 };