mirror of
https://github.com/kunkunsh/kunkun-ext-disk-speed.git
synced 2025-04-03 18:56:44 +00:00
upgrade api
This commit is contained in:
parent
d14e17eb3d
commit
41f95a7af7
4
.gitignore
vendored
4
.gitignore
vendored
@ -23,3 +23,7 @@ extensions_support/
|
|||||||
|
|
||||||
.pnpm-store
|
.pnpm-store
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
speedtest/index.js
|
||||||
|
test.txt
|
||||||
|
target/
|
||||||
|
8
build.ts
Normal file
8
build.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { $ } from "bun";
|
||||||
|
|
||||||
|
await Bun.build({
|
||||||
|
entrypoints: ["speedtest/index.ts"],
|
||||||
|
outdir: "speedtest",
|
||||||
|
minify: true,
|
||||||
|
});
|
||||||
|
await $`vite build`;
|
@ -3,6 +3,6 @@
|
|||||||
"dev": "deno run --watch main.ts"
|
"dev": "deno run --watch main.ts"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@kunkun/api": "jsr:@kunkun/api@^0.0.52"
|
"@kunkun/api": "jsr:@kunkun/api@^0.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
48
deno-src/deno.lock
generated
48
deno-src/deno.lock
generated
@ -1,45 +1,71 @@
|
|||||||
{
|
{
|
||||||
"version": "4",
|
"version": "4",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@kunkun/api@^0.0.39": "0.0.39",
|
"jsr:@kunkun/api@~0.1.7": "0.1.7",
|
||||||
"npm:@types/node@*": "22.5.4",
|
"npm:@types/node@*": "22.5.4",
|
||||||
"npm:kkrpc@^0.0.10": "0.0.10_typescript@5.6.3"
|
"npm:kkrpc@~0.2.2": "0.2.2_typescript@5.8.2"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
"@kunkun/api@0.0.39": {
|
"@kunkun/api@0.1.7": {
|
||||||
"integrity": "af1f0728083a6553279a4a7ce12ca83a6affe7dcda09b041376934e6c26e979e",
|
"integrity": "05522131be509dce77900dfe6ba49fe478deffe73fff18970f6996b2b7c2f0f7",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:kkrpc"
|
"npm:kkrpc"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
|
"@tauri-apps/api@2.3.0": {
|
||||||
|
"integrity": "sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA=="
|
||||||
|
},
|
||||||
|
"@tauri-apps/plugin-shell@2.2.0": {
|
||||||
|
"integrity": "sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==",
|
||||||
|
"dependencies": [
|
||||||
|
"@tauri-apps/api"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@types/node@22.5.4": {
|
"@types/node@22.5.4": {
|
||||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"undici-types"
|
"undici-types"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"kkrpc@0.0.10_typescript@5.6.3": {
|
"copy-anything@3.0.5": {
|
||||||
"integrity": "sha512-lkQKVnN9f6JrS4ybKbGkV4mtuGhWYLTnaWx60ysytEap+sP5jcTbAuJlSrY6JqlwaohiS0X3ZbvJ2rAXYRdTng==",
|
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"is-what"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is-what@4.1.16": {
|
||||||
|
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="
|
||||||
|
},
|
||||||
|
"kkrpc@0.2.2_typescript@5.8.2": {
|
||||||
|
"integrity": "sha512-EliGFPRf+dplMiqNipPUUj89WX9vEWfQkQU05ztbMfdK/SSgnHBbvm7QySGlEIlUb9Y55dSXPkROuxjHz2JbfA==",
|
||||||
|
"dependencies": [
|
||||||
|
"@tauri-apps/plugin-shell",
|
||||||
|
"superjson",
|
||||||
"typescript",
|
"typescript",
|
||||||
"ws"
|
"ws"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"typescript@5.6.3": {
|
"superjson@2.2.2": {
|
||||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="
|
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
|
||||||
|
"dependencies": [
|
||||||
|
"copy-anything"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"typescript@5.8.2": {
|
||||||
|
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="
|
||||||
},
|
},
|
||||||
"undici-types@6.19.8": {
|
"undici-types@6.19.8": {
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
},
|
},
|
||||||
"ws@8.18.0": {
|
"ws@8.18.1": {
|
||||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
|
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@kunkun/api@^0.0.39"
|
"jsr:@kunkun/api@~0.1.7"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
144
deno-src/lib.ts
144
deno-src/lib.ts
@ -1,89 +1,93 @@
|
|||||||
import { expose } from "@kunkun/api/runtime/deno"
|
import { expose } from "@kunkun/api/runtime/deno";
|
||||||
import { API, Progress } from "../src/types.ts"
|
import { API, Progress } from "../src/types.ts";
|
||||||
|
|
||||||
const oneMB = 1024 * 1024
|
const oneMB = 1024 * 1024;
|
||||||
|
|
||||||
export async function sequentialWriteTest(
|
export async function sequentialWriteTest(
|
||||||
options: {
|
options: {
|
||||||
filePath: string
|
filePath: string;
|
||||||
sizeInMB: number
|
sizeInMB: number;
|
||||||
rounds: number
|
rounds: number;
|
||||||
bufferSizeMB: number
|
bufferSizeMB: number;
|
||||||
keepTheFile?: boolean
|
keepTheFile?: boolean;
|
||||||
},
|
},
|
||||||
callback?: (progress: Progress) => void
|
callback?: (progress: Progress) => void
|
||||||
): Promise<Progress> {
|
): Promise<Progress> {
|
||||||
const { filePath, sizeInMB, rounds, bufferSizeMB } = options
|
// console.error("sequentialWriteTest", options);
|
||||||
const data = new Uint8Array(bufferSizeMB * oneMB) // 1MB buffer
|
const { filePath, sizeInMB, rounds, bufferSizeMB } = options;
|
||||||
let start = performance.now()
|
const data = new Uint8Array(bufferSizeMB * oneMB); // 1MB buffer
|
||||||
let totalMB = 0
|
let start = performance.now();
|
||||||
let totalDuration = 0
|
let totalMB = 0;
|
||||||
for (let round = 0; round < rounds; round++) {
|
let totalDuration = 0;
|
||||||
const file = await Deno.open(filePath, { write: true, create: true })
|
for (let round = 0; round < rounds; round++) {
|
||||||
const writer = file.writable.getWriter()
|
const file = await Deno.open(filePath, { write: true, create: true });
|
||||||
|
const writer = file.writable.getWriter();
|
||||||
|
|
||||||
start = performance.now()
|
start = performance.now();
|
||||||
for (let i = 0; i < Math.floor(sizeInMB / bufferSizeMB); i++) {
|
for (let i = 0; i < Math.floor(sizeInMB / bufferSizeMB); i++) {
|
||||||
await writer.write(data)
|
await writer.write(data);
|
||||||
totalMB += bufferSizeMB
|
totalMB += bufferSizeMB;
|
||||||
}
|
}
|
||||||
const roundEnd = performance.now()
|
const roundEnd = performance.now();
|
||||||
totalDuration += (roundEnd - start) / 1000
|
totalDuration += (roundEnd - start) / 1000;
|
||||||
callback?.({ totalMB, totalDuration })
|
callback?.({ totalMB, totalDuration });
|
||||||
await writer.close()
|
await writer.close();
|
||||||
// if keepTheFile, do not remove the file in the last round
|
// if keepTheFile, do not remove the file in the last round
|
||||||
const isLastRound = round === rounds - 1
|
const isLastRound = round === rounds - 1;
|
||||||
if (!isLastRound && !options.keepTheFile) {
|
if (!isLastRound && !options.keepTheFile) {
|
||||||
Deno.removeSync(filePath)
|
Deno.removeSync(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { totalDuration, totalMB }
|
return { totalDuration, totalMB };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createEmptyFile(filePath: string, sizeInMB: number): Promise<void> {
|
export async function createEmptyFile(
|
||||||
if (await fileExists(filePath)) {
|
filePath: string,
|
||||||
await Deno.remove(filePath)
|
sizeInMB: number
|
||||||
}
|
): Promise<void> {
|
||||||
const file = await Deno.open(filePath, { write: true, create: true })
|
if (await fileExists(filePath)) {
|
||||||
const writer = file.writable.getWriter()
|
await Deno.remove(filePath);
|
||||||
for (let i = 0; i < sizeInMB; i++) {
|
}
|
||||||
await writer.write(new Uint8Array(oneMB))
|
const file = await Deno.open(filePath, { write: true, create: true });
|
||||||
}
|
const writer = file.writable.getWriter();
|
||||||
await writer.close()
|
for (let i = 0; i < sizeInMB; i++) {
|
||||||
|
await writer.write(new Uint8Array(oneMB));
|
||||||
|
}
|
||||||
|
await writer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sequential Read
|
// Sequential Read
|
||||||
export async function sequentialReadTest(
|
export async function sequentialReadTest(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
options: { deleteAfter: boolean } = { deleteAfter: true }
|
options: { deleteAfter: boolean } = { deleteAfter: true }
|
||||||
): Promise<Progress> {
|
): Promise<Progress> {
|
||||||
const file = await Deno.open(filePath, { read: true })
|
const file = await Deno.open(filePath, { read: true });
|
||||||
const buffer = new Uint8Array(oneMB) // 1MB buffer
|
const buffer = new Uint8Array(oneMB); // 1MB buffer
|
||||||
const start = performance.now()
|
const start = performance.now();
|
||||||
let totalMB = 0
|
let totalMB = 0;
|
||||||
while ((await file.read(buffer)) !== null) {
|
while ((await file.read(buffer)) !== null) {
|
||||||
totalMB += 1
|
totalMB += 1;
|
||||||
}
|
}
|
||||||
const totalDuration = (performance.now() - start) / 1000
|
const totalDuration = (performance.now() - start) / 1000;
|
||||||
file.close()
|
file.close();
|
||||||
if (options.deleteAfter) {
|
if (options.deleteAfter) {
|
||||||
Deno.removeSync(filePath)
|
Deno.removeSync(filePath);
|
||||||
}
|
}
|
||||||
return { totalMB, totalDuration }
|
return { totalMB, totalDuration };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileExists(filePath: string): boolean {
|
export function fileExists(filePath: string): boolean {
|
||||||
try {
|
try {
|
||||||
Deno.statSync(filePath)
|
Deno.statSync(filePath);
|
||||||
return true
|
return true;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expose({
|
expose({
|
||||||
sequentialWriteTest,
|
sequentialWriteTest,
|
||||||
sequentialReadTest,
|
sequentialReadTest,
|
||||||
createEmptyFile
|
createEmptyFile,
|
||||||
} satisfies API)
|
} satisfies API);
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"tasks": {
|
|
||||||
"dev": "deno run --watch main.ts"
|
|
||||||
},
|
|
||||||
"imports": {
|
|
||||||
"@std/assert": "jsr:@std/assert@1",
|
|
||||||
"@valibot/valibot": "jsr:@valibot/valibot@^0.42.1"
|
|
||||||
}
|
|
||||||
}
|
|
52
dev.ts
52
dev.ts
@ -1,31 +1,27 @@
|
|||||||
import { $ } from "bun"
|
import { sequentialReadTest, sequentialWriteTest } from "./speedtest/lib.ts";
|
||||||
import { DiskSpeedTestInput } from "./src/model.ts"
|
|
||||||
|
|
||||||
const input: DiskSpeedTestInput = {
|
const testPath = "./test.txt";
|
||||||
targetPath: "./testfile.dat",
|
// const testPath = "/Volumes/Portable2TB/test.txt";
|
||||||
sequential: {
|
|
||||||
stressFileSizeMB: 2000
|
|
||||||
},
|
|
||||||
random: {
|
|
||||||
stressFileSizeMB: 1000,
|
|
||||||
iterations: 1000,
|
|
||||||
blockSize: 4096
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const encoded = btoa(JSON.stringify(input))
|
|
||||||
// sequential
|
|
||||||
;(async () => {
|
|
||||||
const res =
|
|
||||||
await $`deno run --allow-read --allow-write deno-scripts/sequential.ts ${encoded}`.quiet()
|
|
||||||
const stdoutSplit = res.stdout.toString("utf-8").split("\n")
|
|
||||||
console.log(JSON.parse(stdoutSplit[stdoutSplit.length - 2]))
|
|
||||||
})()
|
|
||||||
|
|
||||||
// random
|
const writeResult = await sequentialWriteTest(
|
||||||
;(async () => {
|
{
|
||||||
const res = await $`deno run --allow-read --allow-write deno-scripts/random.ts ${encoded}`.quiet()
|
filePath: testPath,
|
||||||
console.log("stdout", res.stdout.toString("utf-8"))
|
sizeInMB: 1000,
|
||||||
|
rounds: 10,
|
||||||
|
bufferSizeMB: 1,
|
||||||
|
keepTheFile: true,
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
console.log(progress);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(writeResult);
|
||||||
|
console.log(writeResult.totalMB / writeResult.totalDuration);
|
||||||
|
|
||||||
const stdoutSplit = res.stdout.toString("utf-8").split("\n")
|
const readResult = await sequentialReadTest({
|
||||||
console.log(JSON.parse(stdoutSplit[stdoutSplit.length - 2]))
|
filePath: testPath,
|
||||||
})()
|
rounds: 3,
|
||||||
|
deleteAfter: false,
|
||||||
|
});
|
||||||
|
console.log(readResult);
|
||||||
|
console.log("read speed: ", readResult.totalMB / readResult.totalDuration);
|
||||||
|
15
package.json
15
package.json
@ -3,7 +3,7 @@
|
|||||||
"name": "kunkun-ext-disk-speed",
|
"name": "kunkun-ext-disk-speed",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "https://github.com/kunkunsh/kunkun-ext-disk-speed",
|
"repository": "https://github.com/kunkunsh/kunkun-ext-disk-speed",
|
||||||
"version": "0.0.6",
|
"version": "0.0.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"kunkun": {
|
"kunkun": {
|
||||||
"name": "Disk Speed",
|
"name": "Disk Speed",
|
||||||
@ -31,7 +31,7 @@
|
|||||||
"permission": "shell:deno:spawn",
|
"permission": "shell:deno:spawn",
|
||||||
"allow": [
|
"allow": [
|
||||||
{
|
{
|
||||||
"path": "$EXTENSION/deno-src/lib.ts",
|
"path": "$EXTENSION/speedtest/index.js",
|
||||||
"read": "*",
|
"read": "*",
|
||||||
"write": "*"
|
"write": "*"
|
||||||
}
|
}
|
||||||
@ -57,17 +57,18 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "bun build.ts",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"
|
"check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/svelte": "^4.0.2",
|
"@iconify/svelte": "^4.0.2",
|
||||||
"@kksh/api": "^0.1.1",
|
"@kksh/api": "^0.1.7",
|
||||||
"@kksh/svelte": "0.1.7",
|
"@kksh/svelte": "0.1.7",
|
||||||
"bits-ui": "^0.21.16",
|
"bits-ui": "^0.21.16",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
|
"kkrpc": "^0.2.2",
|
||||||
"lucide-svelte": "^0.416.0",
|
"lucide-svelte": "^0.416.0",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwind-variants": "^0.2.1",
|
"tailwind-variants": "^0.2.1",
|
||||||
@ -89,9 +90,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
".gitignore",
|
".gitignore",
|
||||||
"deno-src",
|
"speedtest/index.js"
|
||||||
"deno.json",
|
|
||||||
"deno.lock"
|
|
||||||
],
|
],
|
||||||
"packageManager": "pnpm@9.15.3"
|
"packageManager": "pnpm@10.6.5"
|
||||||
}
|
}
|
||||||
|
715
pnpm-lock.yaml
generated
715
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
speedtest-rs/Cargo.lock
generated
Normal file
7
speedtest-rs/Cargo.lock
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "speedtest-rs"
|
||||||
|
version = "0.1.0"
|
6
speedtest-rs/Cargo.toml
Normal file
6
speedtest-rs/Cargo.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "speedtest-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
64
speedtest-rs/src/main.rs
Normal file
64
speedtest-rs/src/main.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, BufReader, Read};
|
||||||
|
use std::time::{Instant, Duration};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn read_file_speed_test(path: &Path) -> io::Result<(u64, Duration)> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let file_size = file.metadata()?.len();
|
||||||
|
let mut reader = BufReader::with_capacity(1024 * 1024, file); // 1MB buffer
|
||||||
|
|
||||||
|
let mut buffer = [0; 1024 * 1024]; // 1MB chunks
|
||||||
|
let mut total_read = 0;
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read(&mut buffer)? {
|
||||||
|
0 => break, // EOF
|
||||||
|
bytes_read => {
|
||||||
|
total_read += bytes_read as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = start.elapsed();
|
||||||
|
|
||||||
|
Ok((total_read, duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_size(bytes: u64) -> String {
|
||||||
|
const KB: u64 = 1024;
|
||||||
|
const MB: u64 = KB * 1024;
|
||||||
|
const GB: u64 = MB * 1024;
|
||||||
|
|
||||||
|
if bytes >= GB {
|
||||||
|
format!("{:.2} GB", bytes as f64 / GB as f64)
|
||||||
|
} else if bytes >= MB {
|
||||||
|
format!("{:.2} MB", bytes as f64 / MB as f64)
|
||||||
|
} else if bytes >= KB {
|
||||||
|
format!("{:.2} KB", bytes as f64 / KB as f64)
|
||||||
|
} else {
|
||||||
|
format!("{} bytes", bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let file_path = Path::new("/Volumes/Portable2TB/test.txt");
|
||||||
|
|
||||||
|
println!("Starting disk read speed test on: {}", file_path.display());
|
||||||
|
|
||||||
|
match read_file_speed_test(file_path) {
|
||||||
|
Ok((bytes_read, duration)) => {
|
||||||
|
let size = format_size(bytes_read);
|
||||||
|
let seconds = duration.as_secs_f64();
|
||||||
|
let speed = bytes_read as f64 / seconds / (1024.0 * 1024.0);
|
||||||
|
|
||||||
|
println!("Read {} in {:.2} seconds", size, seconds);
|
||||||
|
println!("Read speed: {:.2} MB/s", speed);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error testing read speed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
speedtest/index.ts
Normal file
13
speedtest/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { API } from "../src/types.ts";
|
||||||
|
import { expose } from "@kksh/api/runtime/deno";
|
||||||
|
import {
|
||||||
|
sequentialWriteTest,
|
||||||
|
sequentialReadTest,
|
||||||
|
createEmptyFile,
|
||||||
|
} from "./lib.ts";
|
||||||
|
|
||||||
|
expose({
|
||||||
|
sequentialWriteTest,
|
||||||
|
sequentialReadTest,
|
||||||
|
createEmptyFile,
|
||||||
|
} satisfies API);
|
89
speedtest/lib.ts
Normal file
89
speedtest/lib.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
const oneMB = 1024 * 1024;
|
||||||
|
export type Progress = { totalMB: number; totalDuration: number };
|
||||||
|
|
||||||
|
export async function sequentialWriteTest(
|
||||||
|
options: {
|
||||||
|
filePath: string;
|
||||||
|
sizeInMB: number;
|
||||||
|
rounds: number;
|
||||||
|
bufferSizeMB: number;
|
||||||
|
keepTheFile?: boolean;
|
||||||
|
},
|
||||||
|
callback?: (progress: Progress) => void
|
||||||
|
): Promise<Progress> {
|
||||||
|
// console.error("sequentialWriteTest", options);
|
||||||
|
const { filePath, sizeInMB, rounds, bufferSizeMB } = options;
|
||||||
|
const data = new Uint8Array(bufferSizeMB * oneMB); // 1MB buffer
|
||||||
|
let start = performance.now();
|
||||||
|
let totalMB = 0;
|
||||||
|
let totalDuration = 0;
|
||||||
|
for (let round = 0; round < rounds; round++) {
|
||||||
|
const file = await Deno.open(filePath, { write: true, create: true });
|
||||||
|
const writer = file.writable.getWriter();
|
||||||
|
|
||||||
|
start = performance.now();
|
||||||
|
for (let i = 0; i < Math.floor(sizeInMB / bufferSizeMB); i++) {
|
||||||
|
await writer.write(data);
|
||||||
|
totalMB += bufferSizeMB;
|
||||||
|
}
|
||||||
|
const roundEnd = performance.now();
|
||||||
|
totalDuration += (roundEnd - start) / 1000;
|
||||||
|
callback?.({ totalMB, totalDuration });
|
||||||
|
await writer.close();
|
||||||
|
// if keepTheFile, do not remove the file in the last round
|
||||||
|
const isLastRound = round === rounds - 1;
|
||||||
|
if (!isLastRound && !options.keepTheFile) {
|
||||||
|
Deno.removeSync(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { totalDuration, totalMB };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEmptyFile(
|
||||||
|
filePath: string,
|
||||||
|
sizeInMB: number
|
||||||
|
): Promise<void> {
|
||||||
|
if (await fileExists(filePath)) {
|
||||||
|
await Deno.remove(filePath);
|
||||||
|
}
|
||||||
|
const file = await Deno.open(filePath, { write: true, create: true });
|
||||||
|
const writer = file.writable.getWriter();
|
||||||
|
for (let i = 0; i < sizeInMB; i++) {
|
||||||
|
await writer.write(new Uint8Array(oneMB));
|
||||||
|
}
|
||||||
|
await writer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sequential Read
|
||||||
|
export async function sequentialReadTest(
|
||||||
|
options: { filePath: string; rounds: number; deleteAfter: boolean } = {
|
||||||
|
filePath: "",
|
||||||
|
rounds: 1,
|
||||||
|
deleteAfter: true,
|
||||||
|
}
|
||||||
|
): Promise<Progress> {
|
||||||
|
const { filePath, rounds, deleteAfter } = options;
|
||||||
|
const file = await Deno.open(filePath, { read: true });
|
||||||
|
const buffer = new Uint8Array(oneMB); // 1MB buffer
|
||||||
|
const start = performance.now();
|
||||||
|
let totalMB = 0;
|
||||||
|
while ((await file.read(buffer)) !== null) {
|
||||||
|
totalMB += 1;
|
||||||
|
}
|
||||||
|
const totalDuration = (performance.now() - start) / 1000;
|
||||||
|
file.close();
|
||||||
|
if (options.deleteAfter) {
|
||||||
|
Deno.removeSync(filePath);
|
||||||
|
}
|
||||||
|
return { totalMB, totalDuration };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileExists(filePath: string): boolean {
|
||||||
|
try {
|
||||||
|
Deno.statSync(filePath);
|
||||||
|
return true;
|
||||||
|
} catch (_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
239
src/App.svelte
239
src/App.svelte
@ -1,133 +1,142 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { open, path, shell, toast, ui } from "@kksh/api/ui/custom"
|
import { open, path, shell, toast, ui } from "@kksh/api/ui/custom";
|
||||||
import { Button, ModeWatcher, ThemeWrapper, updateTheme } from "@kksh/svelte"
|
import { Button, ModeWatcher, ThemeWrapper, updateTheme } from "@kksh/svelte";
|
||||||
import SpeedGauge from "$lib/components/SpeedGauge.svelte"
|
import SpeedGauge from "$lib/components/SpeedGauge.svelte";
|
||||||
import StressSelect from "$lib/components/StressSelect.svelte"
|
import StressSelect from "$lib/components/StressSelect.svelte";
|
||||||
import TargetDirSelect from "$lib/components/TargetDirSelect.svelte"
|
import TargetDirSelect from "$lib/components/TargetDirSelect.svelte";
|
||||||
import { stress, targetDir } from "$lib/store"
|
import { stress, targetDir } from "$lib/store";
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte";
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store";
|
||||||
import { type API } from "./types"
|
import { type API } from "./types";
|
||||||
|
|
||||||
let readSpeedMBps = $state(0)
|
let readSpeedMBps = $state(0);
|
||||||
let writeSpeedMBps = $state(0)
|
let writeSpeedMBps = $state(0);
|
||||||
let running = $state(false)
|
let running = $state(false);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
ui.registerDragRegion()
|
ui.registerDragRegion();
|
||||||
ui.showBackButton({ right: 0.5, bottom: 0.5 })
|
ui.showBackButton({ right: 0.5, bottom: 0.5 });
|
||||||
|
|
||||||
updateTheme({
|
updateTheme({
|
||||||
theme: "neutral",
|
theme: "neutral",
|
||||||
radius: 0.5,
|
radius: 0.5,
|
||||||
lightMode: "dark"
|
lightMode: "dark",
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
async function startSpeedTest() {
|
async function startSpeedTest() {
|
||||||
running = true
|
running = true;
|
||||||
const _targetDir = get(targetDir)
|
readSpeedMBps;
|
||||||
if (!_targetDir) {
|
writeSpeedMBps;
|
||||||
toast.error("Target directory is not set")
|
const _targetDir = get(targetDir);
|
||||||
return
|
if (!_targetDir) {
|
||||||
}
|
toast.error("Target directory is not set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { rpcChannel, process, command } = await shell.createDenoRpcChannel<
|
||||||
|
{},
|
||||||
|
API
|
||||||
|
>(
|
||||||
|
"$EXTENSION/speedtest/index.js",
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
allowAllRead: true,
|
||||||
|
allowAllWrite: true,
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
const { rpcChannel, process } = await shell.createDenoRpcChannel<{}, API>(
|
const api = rpcChannel.getAPI();
|
||||||
"$EXTENSION/deno-src/lib.ts",
|
const testFileName = "kk-disk-speed-test";
|
||||||
[],
|
|
||||||
{
|
|
||||||
allowAllRead: true,
|
|
||||||
allowAllWrite: true
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
const api = rpcChannel.getAPI()
|
const testFilePath = await path.join(_targetDir, testFileName);
|
||||||
const testFileName = "kk-disk-speed-test"
|
|
||||||
|
|
||||||
const testFilePath = await path.join(_targetDir, testFileName)
|
const writeResult = await api.sequentialWriteTest(
|
||||||
|
{
|
||||||
|
filePath: testFilePath,
|
||||||
|
sizeInMB: get(stress) * 1024,
|
||||||
|
rounds: 3,
|
||||||
|
bufferSizeMB: 1,
|
||||||
|
keepTheFile: true,
|
||||||
|
},
|
||||||
|
({ totalMB, totalDuration }) => {
|
||||||
|
writeSpeedMBps = totalMB / totalDuration;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const readResult = await api.sequentialReadTest({
|
||||||
|
filePath: testFilePath,
|
||||||
|
rounds: 3,
|
||||||
|
deleteAfter: true,
|
||||||
|
});
|
||||||
|
writeSpeedMBps = writeResult.totalMB / writeResult.totalDuration;
|
||||||
|
|
||||||
const writeResult = await api.sequentialWriteTest(
|
readSpeedMBps = readResult.totalMB / readResult.totalDuration;
|
||||||
{
|
|
||||||
filePath: testFilePath,
|
|
||||||
sizeInMB: get(stress) * 1024,
|
|
||||||
rounds: 3,
|
|
||||||
bufferSizeMB: 1,
|
|
||||||
keepTheFile: true
|
|
||||||
},
|
|
||||||
({ totalMB, totalDuration }) => {
|
|
||||||
writeSpeedMBps = totalMB / totalDuration
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const readResult = await api.sequentialReadTest(testFilePath, {
|
|
||||||
deleteAfter: true
|
|
||||||
})
|
|
||||||
writeSpeedMBps = writeResult.totalMB / writeResult.totalDuration
|
|
||||||
console.log("writeDuration", writeResult)
|
|
||||||
|
|
||||||
readSpeedMBps = readResult.totalMB / readResult.totalDuration
|
process
|
||||||
console.log("readDuration", readResult)
|
.kill()
|
||||||
|
.then(() => {
|
||||||
process
|
console.log("process killed");
|
||||||
.kill()
|
})
|
||||||
.then(() => {
|
.catch((err) => {
|
||||||
console.log("process killed")
|
console.error("error killing process", err);
|
||||||
})
|
toast.error(`Error killing process ${process.pid}`);
|
||||||
.catch((err) => {
|
})
|
||||||
console.error("error killing process", err)
|
.finally(() => {
|
||||||
toast.error(`Error killing process ${process.pid}`)
|
running = false;
|
||||||
})
|
});
|
||||||
.finally(() => {
|
}
|
||||||
running = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
ui.goBack()
|
ui.goBack();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
|
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<main class="container flex flex-col gap-4 pt-10">
|
<main class="container flex flex-col gap-4 pt-10">
|
||||||
<div class="absolute left-0 top-0 h-10 w-screen" data-kunkun-drag-region></div>
|
<div
|
||||||
<div class="flex items-center gap-4">
|
class="absolute left-0 top-0 h-10 w-screen"
|
||||||
<strong>Stress</strong>
|
data-kunkun-drag-region
|
||||||
<StressSelect />
|
></div>
|
||||||
</div>
|
<div class="flex items-center gap-4">
|
||||||
<div class="flex items-center gap-4">
|
<strong>Stress</strong>
|
||||||
<strong>Target Directory</strong>
|
<StressSelect />
|
||||||
<TargetDirSelect />
|
</div>
|
||||||
{#if $targetDir}
|
<div class="flex items-center gap-4">
|
||||||
<button
|
<strong>Target Directory</strong>
|
||||||
onclick={() => {
|
<TargetDirSelect />
|
||||||
if ($targetDir) {
|
{#if $targetDir}
|
||||||
open.folder($targetDir)
|
<button
|
||||||
}
|
onclick={() => {
|
||||||
}}
|
if ($targetDir) {
|
||||||
>
|
open.folder($targetDir);
|
||||||
<pre>{$targetDir}</pre>
|
}
|
||||||
</button>
|
}}
|
||||||
{:else}
|
>
|
||||||
<pre class="text-red-500">Pick a target directory to test</pre>
|
<pre>{$targetDir}</pre>
|
||||||
{/if}
|
</button>
|
||||||
</div>
|
{:else}
|
||||||
<Button disabled={!$targetDir || running} on:click={startSpeedTest}>Start Speed Test</Button>
|
<pre class="text-red-500">Pick a target directory to test</pre>
|
||||||
<!-- <div class="flex items-center gap-4">
|
{/if}
|
||||||
<strong>Write Speed</strong>
|
</div>
|
||||||
<pre>{writeSpeedMBps} MB/s</pre>
|
<Button disabled={!$targetDir || running} on:click={startSpeedTest}>
|
||||||
</div>
|
Start Speed Test
|
||||||
<div class="flex items-center gap-4">
|
</Button>
|
||||||
<strong>Read Speed</strong>
|
<div class="grid h-96 w-full grid-cols-2">
|
||||||
<pre>{readSpeedMBps} MB/s</pre>
|
<SpeedGauge
|
||||||
</div> -->
|
speedInMBps={writeSpeedMBps}
|
||||||
<div class="grid h-96 w-full grid-cols-2">
|
title="Write Speed"
|
||||||
<SpeedGauge speedInMBps={writeSpeedMBps} title="Write Speed" class="h-full w-full" />
|
class="h-full w-full"
|
||||||
<SpeedGauge speedInMBps={readSpeedMBps} title="Read Speed" class="h-full w-full" />
|
/>
|
||||||
</div>
|
<SpeedGauge
|
||||||
</main>
|
speedInMBps={readSpeedMBps}
|
||||||
|
title="Read Speed"
|
||||||
|
class="h-full w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
export const stress = writable(1)
|
export const stress = writable(1);
|
||||||
export const targetDir = writable<string | undefined>(undefined)
|
export const targetDir = writable<string | undefined>("/Volumes/Portable2TB");
|
||||||
|
23
src/types.ts
23
src/types.ts
@ -1,15 +1,12 @@
|
|||||||
export type Progress = { totalMB: number; totalDuration: number }
|
import type {
|
||||||
|
createEmptyFile,
|
||||||
|
sequentialReadTest,
|
||||||
|
sequentialWriteTest,
|
||||||
|
} from "../speedtest/lib.ts";
|
||||||
|
|
||||||
export interface API {
|
export interface API {
|
||||||
sequentialWriteTest: (
|
sequentialWriteTest: typeof sequentialWriteTest;
|
||||||
options: {
|
sequentialReadTest: typeof sequentialReadTest;
|
||||||
filePath: string
|
createEmptyFile: typeof createEmptyFile;
|
||||||
sizeInMB: number
|
|
||||||
rounds: number
|
|
||||||
bufferSizeMB: number
|
|
||||||
keepTheFile?: boolean
|
|
||||||
},
|
|
||||||
callback?: (progress: Progress) => void
|
|
||||||
) => Promise<Progress>
|
|
||||||
sequentialReadTest: (filePath: string, options: { deleteAfter: boolean }) => Promise<Progress>
|
|
||||||
createEmptyFile: (filePath: string, sizeInMB: number) => Promise<void>
|
|
||||||
}
|
}
|
||||||
|
export type { Progress } from "../speedtest/lib.ts";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user