mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-04-29 12:06:01 +02:00
perf(fs): improve FileHandle.read performance (#1950)
* perf(fs): improve `FileHandle.read` performance * handle different target pointer width * improve `writeTextFile` performance * revert packageManager field * change file --------- Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"fs": patch
|
||||
"fs-js": patch
|
||||
---
|
||||
|
||||
Improve performance of the `FileHandle.read` and `writeTextFile` APIs.
|
||||
File diff suppressed because one or more lines are too long
@@ -243,6 +243,25 @@ function parseFileInfo(r: UnparsedFileInfo): FileInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// https://mstn.github.io/2018/06/08/fixed-size-arrays-in-typescript/
|
||||
type FixedSizeArray<T, N extends number> = ReadonlyArray<T> & {
|
||||
length: N
|
||||
}
|
||||
|
||||
// https://gist.github.com/zapthedingbat/38ebfbedd98396624e5b5f2ff462611d
|
||||
/** Converts a big-endian eight byte array to number */
|
||||
function fromBytes(buffer: FixedSizeArray<number, 8>): number {
|
||||
const bytes = new Uint8ClampedArray(buffer)
|
||||
const size = bytes.byteLength
|
||||
let x = 0
|
||||
for (let i = 0; i < size; i++) {
|
||||
const byte = bytes[i]
|
||||
x *= 0x100
|
||||
x += byte
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
/**
|
||||
* The Tauri abstraction for reading and writing files.
|
||||
*
|
||||
@@ -285,12 +304,20 @@ class FileHandle extends Resource {
|
||||
return 0
|
||||
}
|
||||
|
||||
const [data, nread] = await invoke<[number[], number]>('plugin:fs|read', {
|
||||
const data = await invoke<ArrayBuffer | number[]>('plugin:fs|read', {
|
||||
rid: this.rid,
|
||||
len: buffer.byteLength
|
||||
})
|
||||
|
||||
buffer.set(data)
|
||||
// Rust side will never return an empty array for this command and
|
||||
// ensure there is at least 8 elements there.
|
||||
//
|
||||
// This is an optimization to include the number of read bytes (as bigendian bytes)
|
||||
// at the end of returned array to avoid serialization overhead of separate values.
|
||||
const nread = fromBytes(data.slice(-8) as FixedSizeArray<number, 8>)
|
||||
|
||||
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data
|
||||
buffer.set(bytes.slice(0, bytes.length - 8))
|
||||
|
||||
return nread === 0 ? null : nread
|
||||
}
|
||||
@@ -1041,10 +1068,13 @@ async function writeTextFile(
|
||||
throw new TypeError('Must be a file URL.')
|
||||
}
|
||||
|
||||
await invoke('plugin:fs|write_text_file', {
|
||||
path: path instanceof URL ? path.toString() : path,
|
||||
data,
|
||||
options
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
await invoke('plugin:fs|write_text_file', encoder.encode(data), {
|
||||
headers: {
|
||||
path: path instanceof URL ? path.toString() : path,
|
||||
options: JSON.stringify(options)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+61
-50
@@ -9,7 +9,7 @@ use tauri::{
|
||||
ipc::{CommandScope, GlobalScope},
|
||||
path::BaseDirectory,
|
||||
utils::config::FsScope,
|
||||
AppHandle, Manager, Resource, ResourceId, Runtime, Webview,
|
||||
Manager, Resource, ResourceId, Runtime, Webview,
|
||||
};
|
||||
|
||||
use std::{
|
||||
@@ -301,13 +301,34 @@ pub async fn read_dir<R: Runtime>(
|
||||
pub async fn read<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
rid: ResourceId,
|
||||
len: u32,
|
||||
) -> CommandResult<(Vec<u8>, usize)> {
|
||||
let mut data = vec![0; len as usize];
|
||||
len: usize,
|
||||
) -> CommandResult<tauri::ipc::Response> {
|
||||
let mut data = vec![0; len];
|
||||
let file = webview.resources_table().get::<StdFileResource>(rid)?;
|
||||
let nread = StdFileResource::with_lock(&file, |mut file| file.read(&mut data))
|
||||
.map_err(|e| format!("faied to read bytes from file with error: {e}"))?;
|
||||
Ok((data, nread))
|
||||
|
||||
// This is an optimization to include the number of read bytes (as bigendian bytes)
|
||||
// at the end of returned vector so we can use `tauri::ipc::Response`
|
||||
// and avoid serialization overhead of separate values.
|
||||
#[cfg(target_pointer_width = "16")]
|
||||
let nread = {
|
||||
let nread = nread.to_be_bytes();
|
||||
let mut out = [0; 8];
|
||||
out[6..].copy_from_slice(&nread);
|
||||
};
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
let nread = {
|
||||
let nread = nread.to_be_bytes();
|
||||
let mut out = [0; 8];
|
||||
out[4..].copy_from_slice(&nread);
|
||||
};
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
let nread = nread.to_be_bytes();
|
||||
|
||||
data.extend(nread);
|
||||
|
||||
Ok(tauri::ipc::Response::new(data))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -783,10 +804,34 @@ fn write_file_inner<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
global_scope: &GlobalScope<Entry>,
|
||||
command_scope: &CommandScope<Entry>,
|
||||
path: SafeFilePath,
|
||||
data: &[u8],
|
||||
options: Option<WriteFileOptions>,
|
||||
request: tauri::ipc::Request<'_>,
|
||||
) -> CommandResult<()> {
|
||||
let data = match request.body() {
|
||||
tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data),
|
||||
tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned(
|
||||
data.iter()
|
||||
.flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8)))
|
||||
.collect(),
|
||||
),
|
||||
_ => return Err(anyhow::anyhow!("unexpected invoke body").into()),
|
||||
};
|
||||
|
||||
let path = request
|
||||
.headers()
|
||||
.get("path")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing file path").into())
|
||||
.and_then(|p| {
|
||||
percent_encoding::percent_decode(p.as_ref())
|
||||
.decode_utf8()
|
||||
.map_err(|_| anyhow::anyhow!("path is not a valid UTF-8").into())
|
||||
})
|
||||
.and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?;
|
||||
let options: Option<WriteFileOptions> = request
|
||||
.headers()
|
||||
.get("options")
|
||||
.and_then(|p| p.to_str().ok())
|
||||
.and_then(|opts| serde_json::from_str(opts).ok());
|
||||
|
||||
let (mut file, path) = resolve_file(
|
||||
&webview,
|
||||
global_scope,
|
||||
@@ -823,7 +868,7 @@ fn write_file_inner<R: Runtime>(
|
||||
},
|
||||
)?;
|
||||
|
||||
file.write_all(data)
|
||||
file.write_all(&data)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"failed to write bytes to file at path: {} with error: {e}",
|
||||
@@ -840,52 +885,18 @@ pub async fn write_file<R: Runtime>(
|
||||
command_scope: CommandScope<Entry>,
|
||||
request: tauri::ipc::Request<'_>,
|
||||
) -> CommandResult<()> {
|
||||
let data = match request.body() {
|
||||
tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data),
|
||||
tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned(
|
||||
data.iter()
|
||||
.flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8)))
|
||||
.collect(),
|
||||
),
|
||||
_ => return Err(anyhow::anyhow!("unexpected invoke body").into()),
|
||||
};
|
||||
|
||||
let path = request
|
||||
.headers()
|
||||
.get("path")
|
||||
.ok_or_else(|| anyhow::anyhow!("missing file path").into())
|
||||
.and_then(|p| {
|
||||
percent_encoding::percent_decode(p.as_ref())
|
||||
.decode_utf8()
|
||||
.map_err(|_| anyhow::anyhow!("path is not a valid UTF-8").into())
|
||||
})
|
||||
.and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?;
|
||||
let options = request
|
||||
.headers()
|
||||
.get("options")
|
||||
.and_then(|p| p.to_str().ok())
|
||||
.and_then(|opts| serde_json::from_str(opts).ok());
|
||||
write_file_inner(webview, &global_scope, &command_scope, path, &data, options)
|
||||
write_file_inner(webview, &global_scope, &command_scope, request)
|
||||
}
|
||||
|
||||
// TODO, in v3, remove this command and rely on `write_file` command only
|
||||
#[tauri::command]
|
||||
pub async fn write_text_file<R: Runtime>(
|
||||
#[allow(unused)] app: AppHandle<R>,
|
||||
#[allow(unused)] webview: Webview<R>,
|
||||
#[allow(unused)] global_scope: GlobalScope<Entry>,
|
||||
#[allow(unused)] command_scope: CommandScope<Entry>,
|
||||
path: SafeFilePath,
|
||||
data: String,
|
||||
#[allow(unused)] options: Option<WriteFileOptions>,
|
||||
webview: Webview<R>,
|
||||
global_scope: GlobalScope<Entry>,
|
||||
command_scope: CommandScope<Entry>,
|
||||
request: tauri::ipc::Request<'_>,
|
||||
) -> CommandResult<()> {
|
||||
write_file_inner(
|
||||
webview,
|
||||
&global_scope,
|
||||
&command_scope,
|
||||
path,
|
||||
data.as_bytes(),
|
||||
options,
|
||||
)
|
||||
write_file_inner(webview, &global_scope, &command_scope, request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
Reference in New Issue
Block a user