Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/browser-demos/pages/sqlite-test/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ let kernelBytes: ArrayBuffer | null = null;
let vfsImageBytes: Uint8Array | null = null;
let testfixtureBytes: ArrayBuffer | null = null;

const DEFAULT_SQLITE_MAX_MEMORY_PAGES = 4096;

function sqliteMaxMemoryPages(): number {
const raw = import.meta.env.VITE_SQLITE_BROWSER_MAX_MEMORY_PAGES;
if (raw == null || raw === "") return DEFAULT_SQLITE_MAX_MEMORY_PAGES;
const pages = Number(raw);
if (!Number.isInteger(pages) || pages <= 0) {
throw new Error(`Invalid VITE_SQLITE_BROWSER_MAX_MEMORY_PAGES: ${raw}`);
}
return pages;
}

function readVfsFile(fs: MemoryFileSystem, path: string): Uint8Array {
const st = fs.stat(path);
const fd = fs.open(path, 0, 0);
Expand Down Expand Up @@ -137,6 +149,7 @@ async function init() {
const fixture = readVfsFile(fs, "/usr/bin/testfixture");
testfixtureBytes = new ArrayBuffer(fixture.byteLength);
new Uint8Array(testfixtureBytes).set(fixture);
const maxMemoryPages = sqliteMaxMemoryPages();

async function runSqlite(argv: string[], label: string, timeoutMs = 180_000, options: SqliteRunOptions = {}): Promise<SqliteTestResult> {
const start = performance.now();
Expand Down Expand Up @@ -175,6 +188,11 @@ async function init() {
const kernel = new BrowserKernel({
memfs: fs,
maxWorkers: 4,
// The official SQLite testrunner starts hundreds of short-lived
// testfixture workers in one browser page. Chromium reserves each
// shared Wasm memory up to its maximum, and the 1 GiB host default can
// exhaust renderer address space before the full suite completes.
maxMemoryPages,
enableSyscallLog: import.meta.env.VITE_SQLITE_BROWSER_SYSCALL_LOG === "1",
syscallLogPtrWidth: sqliteSyscallLogPtrWidth(),
onStdout: (data) => { appendStdout(new TextDecoder().decode(data)); },
Expand Down
30 changes: 30 additions & 0 deletions crates/kernel/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,36 @@ pub trait HostIO {
fn host_read(&mut self, handle: i64, buf: &mut [u8]) -> Result<usize, Errno>;
fn host_write(&mut self, handle: i64, buf: &[u8]) -> Result<usize, Errno>;
fn host_seek(&mut self, handle: i64, offset: i64, whence: u32) -> Result<i64, Errno>;
fn host_pread(&mut self, handle: i64, buf: &mut [u8], offset: i64) -> Result<usize, Errno> {
if offset < 0 {
return Err(Errno::EINVAL);
}
let saved_offset = self.host_seek(handle, 0, 1)?;
let result = self
.host_seek(handle, offset, 0)
.and_then(|_| self.host_read(handle, buf));
let restore_result = self.host_seek(handle, saved_offset, 0);
match (result, restore_result) {
(Ok(n), Ok(_)) => Ok(n),
(Err(e), _) => Err(e),
(Ok(_), Err(e)) => Err(e),
}
}
fn host_pwrite(&mut self, handle: i64, buf: &[u8], offset: i64) -> Result<usize, Errno> {
if offset < 0 {
return Err(Errno::EINVAL);
}
let saved_offset = self.host_seek(handle, 0, 1)?;
let result = self
.host_seek(handle, offset, 0)
.and_then(|_| self.host_write(handle, buf));
let restore_result = self.host_seek(handle, saved_offset, 0);
match (result, restore_result) {
(Ok(n), Ok(_)) => Ok(n),
(Err(e), _) => Err(e),
(Ok(_), Err(e)) => Err(e),
}
}
fn host_fstat(&mut self, handle: i64) -> Result<WasmStat, Errno>;
fn host_stat(&mut self, path: &[u8]) -> Result<WasmStat, Errno>;
fn host_lstat(&mut self, path: &[u8]) -> Result<WasmStat, Errno>;
Expand Down
85 changes: 71 additions & 14 deletions crates/kernel/src/syscalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1981,7 +1981,6 @@ pub fn sys_pread(
}

let host_handle = ofd.host_handle;
let saved_offset = ofd.offset;

if host_handle == SYNTHETIC_FILE_HANDLE {
let data = synthetic_file_content(&ofd.path).ok_or(Errno::EBADF)?;
Expand All @@ -1994,13 +1993,7 @@ pub fn sys_pread(
return Ok(n);
}

// Seek to the requested offset, read, then restore.
// Single-threaded, so save/seek/read/restore is safe.
host.host_seek(host_handle, offset, SEEK_SET)?;
let n = host.host_read(host_handle, buf)?;
host.host_seek(host_handle, saved_offset, SEEK_SET)?;

Ok(n)
host.host_pread(host_handle, buf, offset)
}

/// Write to a file descriptor at a given offset without modifying the file position.
Expand Down Expand Up @@ -2038,13 +2031,8 @@ pub fn sys_pwrite(
}

let host_handle = ofd.host_handle;
let saved_offset = ofd.offset;

host.host_seek(host_handle, offset, SEEK_SET)?;
let n = host.host_write(host_handle, buf)?;
host.host_seek(host_handle, saved_offset, SEEK_SET)?;

Ok(n)
host.host_pwrite(host_handle, buf, offset)
}

/// preadv -- scatter-gather read at offset.
Expand Down Expand Up @@ -9775,6 +9763,11 @@ mod tests {
handle_paths: std::collections::HashMap<i64, Vec<u8>>,
missing_paths: std::collections::HashSet<Vec<u8>>,
statfs_by_path: std::collections::HashMap<Vec<u8>, WasmStatfs>,
read_calls: usize,
write_calls: usize,
seek_calls: usize,
pread_calls: usize,
pwrite_calls: usize,
}

impl MockHostIO {
Expand All @@ -9793,6 +9786,11 @@ mod tests {
handle_paths: std::collections::HashMap::new(),
missing_paths: std::collections::HashSet::new(),
statfs_by_path: std::collections::HashMap::new(),
read_calls: 0,
write_calls: 0,
seek_calls: 0,
pread_calls: 0,
pwrite_calls: 0,
}
}

Expand Down Expand Up @@ -9856,20 +9854,45 @@ mod tests {
}

fn host_read(&mut self, _handle: i64, buf: &mut [u8]) -> Result<usize, Errno> {
self.read_calls += 1;
let data = b"hello";
let n = buf.len().min(data.len());
buf[..n].copy_from_slice(&data[..n]);
Ok(n)
}

fn host_write(&mut self, _handle: i64, buf: &[u8]) -> Result<usize, Errno> {
self.write_calls += 1;
Ok(buf.len())
}

fn host_seek(&mut self, _handle: i64, _offset: i64, _whence: u32) -> Result<i64, Errno> {
self.seek_calls += 1;
Ok(0)
}

fn host_pread(
&mut self,
_handle: i64,
buf: &mut [u8],
offset: i64,
) -> Result<usize, Errno> {
self.pread_calls += 1;
let data = b"hello";
let start = offset as usize;
if start >= data.len() {
return Ok(0);
}
let n = buf.len().min(data.len() - start);
buf[..n].copy_from_slice(&data[start..start + n]);
Ok(n)
}

fn host_pwrite(&mut self, _handle: i64, buf: &[u8], _offset: i64) -> Result<usize, Errno> {
self.pwrite_calls += 1;
Ok(buf.len())
}

fn host_fstat(&mut self, handle: i64) -> Result<WasmStat, Errno> {
let (uid, gid) = self.handle_owners.get(&handle).copied().unwrap_or((0, 0));
let mode = self
Expand Down Expand Up @@ -12821,6 +12844,40 @@ mod tests {
);
}

#[test]
fn test_pread_uses_positioned_host_io_without_seeking() {
let mut proc = Process::new(1);
let mut host = MockHostIO::new();
let fd = sys_open(&mut proc, &mut host, b"/tmp/f", O_RDWR | O_CREAT, 0o644).unwrap();
let ofd_idx = proc.fd_table.get(fd).unwrap().ofd_ref.0;
proc.ofd_table.get_mut(ofd_idx).unwrap().offset = 77;

let mut buf = [0u8; 3];
assert_eq!(sys_pread(&mut proc, &mut host, fd, &mut buf, 1), Ok(3));

assert_eq!(&buf, b"ell");
assert_eq!(host.pread_calls, 1);
assert_eq!(host.read_calls, 0);
assert_eq!(host.seek_calls, 0);
assert_eq!(proc.ofd_table.get(ofd_idx).unwrap().offset, 77);
}

#[test]
fn test_pwrite_uses_positioned_host_io_without_seeking() {
let mut proc = Process::new(1);
let mut host = MockHostIO::new();
let fd = sys_open(&mut proc, &mut host, b"/tmp/f", O_RDWR | O_CREAT, 0o644).unwrap();
let ofd_idx = proc.fd_table.get(fd).unwrap().ofd_ref.0;
proc.ofd_table.get_mut(ofd_idx).unwrap().offset = 88;

assert_eq!(sys_pwrite(&mut proc, &mut host, fd, b"xyz", 2), Ok(3));

assert_eq!(host.pwrite_calls, 1);
assert_eq!(host.write_calls, 0);
assert_eq!(host.seek_calls, 0);
assert_eq!(proc.ofd_table.get(ofd_idx).unwrap().offset, 88);
}

#[test]
fn test_pread_espipe_on_socket() {
let mut proc = Process::new(1);
Expand Down
51 changes: 51 additions & 0 deletions crates/kernel/src/wasm_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ unsafe extern "C" {
fn host_read(handle: i64, buf_ptr: *mut u8, buf_len: u32) -> i32;
fn host_write(handle: i64, buf_ptr: *const u8, buf_len: u32) -> i32;
fn host_seek(handle: i64, offset_lo: u32, offset_hi: i32, whence: u32) -> i64;
fn host_pread(
handle: i64,
buf_ptr: *mut u8,
buf_len: u32,
offset_lo: u32,
offset_hi: i32,
) -> i32;
fn host_pwrite(
handle: i64,
buf_ptr: *const u8,
buf_len: u32,
offset_lo: u32,
offset_hi: i32,
) -> i32;
fn host_fstat(handle: i64, stat_ptr: *mut u8) -> i32;
fn host_stat(path_ptr: *const u8, path_len: u32, stat_ptr: *mut u8) -> i32;
fn host_lstat(path_ptr: *const u8, path_len: u32, stat_ptr: *mut u8) -> i32;
Expand Down Expand Up @@ -241,6 +255,43 @@ impl HostIO for WasmHostIO {
}
}

fn host_pread(&mut self, handle: i64, buf: &mut [u8], offset: i64) -> Result<usize, Errno> {
let offset_lo = offset as u32;
let offset_hi = (offset >> 32) as i32;
let result = unsafe {
host_pread(
handle,
buf.as_mut_ptr(),
buf.len() as u32,
offset_lo,
offset_hi,
)
};
if result < 0 {
match Errno::from_u32((-result) as u32) {
Some(e) => Err(e),
None => Err(Errno::EIO),
}
} else {
Ok(result as usize)
}
}

fn host_pwrite(&mut self, handle: i64, buf: &[u8], offset: i64) -> Result<usize, Errno> {
let offset_lo = offset as u32;
let offset_hi = (offset >> 32) as i32;
let result =
unsafe { host_pwrite(handle, buf.as_ptr(), buf.len() as u32, offset_lo, offset_hi) };
if result < 0 {
match Errno::from_u32((-result) as u32) {
Some(e) => Err(e),
None => Err(Errno::EIO),
}
} else {
Ok(result as usize)
}
}

fn host_fstat(&mut self, handle: i64) -> Result<WasmStat, Errno> {
let mut stat = WasmStat {
st_dev: 0,
Expand Down
6 changes: 5 additions & 1 deletion docs/porting-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,11 @@ Use `--explain` to ask SQLite's testrunner to print the planned jobs without
starting a full permutation run. Browser runs launch the SQLite-only demo page
through Vite with `KANDELO_BROWSER_DEMO_INPUTS=sqlite-test` and disable HMR
with `KANDELO_BROWSER_TEST_NO_HMR=1` so long test runs do not churn on
artifact writes.
artifact writes. Browser runs also cap each Wasm process at 4096 64KiB pages
(256MiB) by default to keep Chromium from reserving the host default 1GiB for
each short-lived SQLite testfixture worker. Override with
`SQLITE_BROWSER_MAX_MEMORY_PAGES=<pages>` when investigating memory-sensitive
SQLite cases.

## Troubleshooting

Expand Down
Loading
Loading