v0.0.7 - Disable perf probe by default (fix 322% WebProcess CPU), gate behind setting, top-frame only, quiet console logging
This commit is contained in:
Generated
+1
-1
@@ -3056,7 +3056,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sovereign_browser"
|
name = "sovereign_browser"
|
||||||
version = "0.0.5"
|
version = "0.0.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ members = [
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "sovereign_browser"
|
name = "sovereign_browser"
|
||||||
version = "0.0.6"
|
version = "0.0.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "A Linux x86 web browser built on WebKitGTK with Nostr identity"
|
description = "A Linux x86 web browser built on WebKitGTK with Nostr identity"
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ set -e
|
|||||||
# run.sh - Build (only if needed) and run the sovereign_browser binary.
|
# run.sh - Build (only if needed) and run the sovereign_browser binary.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./run.sh [args...] # build if stale, then run with args
|
# ./run.sh [args...] # build if stale (debug), then run with args
|
||||||
|
# ./run.sh --release # build/run the optimized release binary instead
|
||||||
# ./run.sh --build # force a rebuild, then run
|
# ./run.sh --build # force a rebuild, then run
|
||||||
# ./run.sh --no-build # run without building (fails if missing)
|
# ./run.sh --no-build # run without building (fails if missing)
|
||||||
#
|
#
|
||||||
|
# The default profile is DEBUG: it rebuilds in a few seconds, which is what
|
||||||
|
# you want while iterating. Pass --release for the optimized binary (much
|
||||||
|
# slower to build, faster at runtime).
|
||||||
|
#
|
||||||
# The binary is rebuilt only when it is missing or when any source file
|
# The binary is rebuilt only when it is missing or when any source file
|
||||||
# (src/**, Cargo.toml, Cargo.lock, www/**) is newer than the binary.
|
# (src/**, Cargo.toml, Cargo.lock, www/**) is newer than the binary.
|
||||||
|
|
||||||
@@ -29,15 +34,18 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
BIN_NAME="sovereign_browser"
|
BIN_NAME="sovereign_browser"
|
||||||
BIN_PATH="target/release/${BIN_NAME}"
|
|
||||||
|
|
||||||
# --- Args ---------------------------------------------------------------
|
# --- Args ---------------------------------------------------------------
|
||||||
FORCE_BUILD=false
|
FORCE_BUILD=false
|
||||||
NO_BUILD=false
|
NO_BUILD=false
|
||||||
|
RELEASE=false
|
||||||
RUN_ARGS=()
|
RUN_ARGS=()
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
|
--release)
|
||||||
|
RELEASE=true
|
||||||
|
;;
|
||||||
--build)
|
--build)
|
||||||
FORCE_BUILD=true
|
FORCE_BUILD=true
|
||||||
;;
|
;;
|
||||||
@@ -50,6 +58,17 @@ for arg in "$@"; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# --- Profile ------------------------------------------------------------
|
||||||
|
# Debug is the default; --release selects the optimized profile.
|
||||||
|
if [[ "$RELEASE" == true ]]; then
|
||||||
|
PROFILE_DIR="release"
|
||||||
|
CARGO_PROFILE_ARGS=(--release)
|
||||||
|
else
|
||||||
|
PROFILE_DIR="debug"
|
||||||
|
CARGO_PROFILE_ARGS=()
|
||||||
|
fi
|
||||||
|
BIN_PATH="target/${PROFILE_DIR}/${BIN_NAME}"
|
||||||
|
|
||||||
# --- Determine whether a build is needed --------------------------------
|
# --- Determine whether a build is needed --------------------------------
|
||||||
needs_build() {
|
needs_build() {
|
||||||
# No binary yet -> must build.
|
# No binary yet -> must build.
|
||||||
@@ -70,12 +89,12 @@ if [[ "$NO_BUILD" == true ]]; then
|
|||||||
fi
|
fi
|
||||||
print_status "Skipping build (--no-build)."
|
print_status "Skipping build (--no-build)."
|
||||||
elif [[ "$FORCE_BUILD" == true ]]; then
|
elif [[ "$FORCE_BUILD" == true ]]; then
|
||||||
print_status "Forcing rebuild of ${BIN_NAME} (release)..."
|
print_status "Forcing rebuild of ${BIN_NAME} (${PROFILE_DIR})..."
|
||||||
cargo build --release
|
cargo build "${CARGO_PROFILE_ARGS[@]}"
|
||||||
print_success "Build complete."
|
print_success "Build complete."
|
||||||
elif needs_build; then
|
elif needs_build; then
|
||||||
print_status "Sources changed or binary missing; building ${BIN_NAME} (release)..."
|
print_status "Sources changed or binary missing; building ${BIN_NAME} (${PROFILE_DIR})..."
|
||||||
cargo build --release
|
cargo build "${CARGO_PROFILE_ARGS[@]}"
|
||||||
print_success "Build complete."
|
print_success "Build complete."
|
||||||
else
|
else
|
||||||
print_status "Binary is up to date; skipping build."
|
print_status "Binary is up to date; skipping build."
|
||||||
|
|||||||
+179
-31
@@ -62,8 +62,12 @@ fn handle_sovereign_scheme(request: &URISchemeRequest) {
|
|||||||
let route = path.split('?').next().unwrap_or(path);
|
let route = path.split('?').next().unwrap_or(path);
|
||||||
|
|
||||||
// ── nostr API ───────────────────────────────────────────────────
|
// ── nostr API ───────────────────────────────────────────────────
|
||||||
if route.starts_with("nostr/") {
|
// NOTE: pass `path` (which still includes the query string), not
|
||||||
handle_nostr_api(request, &route[6..]);
|
// `route`. The JS shim encodes the request body as `?body=<json>`,
|
||||||
|
// so stripping the query here would make every body-bearing method
|
||||||
|
// (signEvent, nip04/nip44) fail with "Invalid request body".
|
||||||
|
if path.starts_with("nostr/") {
|
||||||
|
handle_nostr_api(request, &path[6..]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,6 +531,16 @@ fn handle_settings_set(request: &URISchemeRequest, query: &str) {
|
|||||||
let _ = db::db_kv_set("agent_server_enabled", new);
|
let _ = db::db_kv_set("agent_server_enabled", new);
|
||||||
(new == "true", true)
|
(new == "true", true)
|
||||||
}
|
}
|
||||||
|
"perf_probe_enabled" => {
|
||||||
|
// Toggle the per-tab performance probe. OFF by default; the
|
||||||
|
// probe patches hot JS APIs and runs a permanent rAF loop.
|
||||||
|
// Takes effect on the next tab (or reload).
|
||||||
|
let mut s = settings::settings_get();
|
||||||
|
s.perf_probe_enabled = !s.perf_probe_enabled;
|
||||||
|
settings::settings_update(&serde_json::json!({"perf_probe_enabled": s.perf_probe_enabled}));
|
||||||
|
settings::settings_save();
|
||||||
|
(s.perf_probe_enabled, false)
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
respond_error_json(request, 400, "Unknown feature");
|
respond_error_json(request, 400, "Unknown feature");
|
||||||
return;
|
return;
|
||||||
@@ -687,6 +701,7 @@ fn handle_settings_config_json(request: &URISchemeRequest) {
|
|||||||
"dev_extras": true,
|
"dev_extras": true,
|
||||||
"file_access": true,
|
"file_access": true,
|
||||||
"universal_access": true,
|
"universal_access": true,
|
||||||
|
"perf_probe_enabled": s.perf_probe_enabled,
|
||||||
"search_engines": search_engines,
|
"search_engines": search_engines,
|
||||||
"shortcuts": shortcuts,
|
"shortcuts": shortcuts,
|
||||||
});
|
});
|
||||||
@@ -721,21 +736,27 @@ fn handle_nostr_api(request: &URISchemeRequest, method: &str) {
|
|||||||
respond_error_json(request, 403, "Read-only mode");
|
respond_error_json(request, 403, "Read-only mode");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(ref signer) = state.signer {
|
let signer = match &state.signer {
|
||||||
match serde_json::from_str::<nostr_core::types::Event>(&event_json) {
|
Some(s) => s.clone(),
|
||||||
Ok(event) => {
|
None => {
|
||||||
match signer.sign_event(&event) {
|
respond_error_json(request, 500, "No signer available");
|
||||||
Ok(signed) => {
|
return;
|
||||||
let result = serde_json::to_string(&signed).unwrap_or_default();
|
|
||||||
respond_json(request, &result);
|
|
||||||
}
|
|
||||||
Err(e) => respond_error_json(request, 500, &e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => respond_error_json(request, 400, &e.to_string()),
|
|
||||||
}
|
}
|
||||||
} else {
|
};
|
||||||
respond_error_json(request, 500, "No signer available");
|
let pubkey_hex = state.pubkey_hex.clone();
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
match parse_unsigned_event(&event_json, &pubkey_hex) {
|
||||||
|
Ok(event) => {
|
||||||
|
match signer.sign_event(&event) {
|
||||||
|
Ok(signed) => {
|
||||||
|
let result = serde_json::to_string(&signed).unwrap_or_default();
|
||||||
|
respond_json(request, &result);
|
||||||
|
}
|
||||||
|
Err(e) => respond_error_json(request, 500, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => respond_error_json(request, 400, &e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"getRelays" => {
|
"getRelays" => {
|
||||||
@@ -817,22 +838,31 @@ fn handle_nostr_api(request: &URISchemeRequest, method: &str) {
|
|||||||
let plaintext = params.get("plaintext").and_then(|v| v.as_str()).unwrap_or("");
|
let plaintext = params.get("plaintext").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
match signer.nip44_encrypt(&peer_pk, plaintext) {
|
match signer.nip44_encrypt(&peer_pk, plaintext) {
|
||||||
Ok(ciphertext) => {
|
Ok(ciphertext) => {
|
||||||
let json = serde_json::json!({ "result": ciphertext });
|
// NIP-44 ciphertext is raw bytes; the NIP-07 API
|
||||||
|
// (and the C bridge) represent it as a base64
|
||||||
|
// string. Serializing the Vec<u8> directly would
|
||||||
|
// emit a JSON array of numbers instead.
|
||||||
|
let b64 = nostr_core::util::base64_encode(&ciphertext);
|
||||||
|
let json = serde_json::json!({ "result": b64 });
|
||||||
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
|
respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
|
||||||
}
|
}
|
||||||
Err(e) => respond_error_json(request, 500, &e.to_string()),
|
Err(e) => respond_error_json(request, 500, &e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"nip44Decrypt" => {
|
"nip44Decrypt" => {
|
||||||
let ciphertext_bytes: Vec<u8> = params.get("ciphertext").map(|v| {
|
// The JS shim sends the ciphertext as a base64 string
|
||||||
if let Some(s) = v.as_str() {
|
// (NIP-07 convention). The signer expects the raw binary
|
||||||
s.as_bytes().to_vec()
|
// payload, so base64-decode it. Passing the base64 text's
|
||||||
} else if let Some(arr) = v.as_array() {
|
// ASCII bytes through unchanged corrupts the payload and
|
||||||
arr.iter().filter_map(|b| b.as_u64().map(|n| n as u8)).collect()
|
// makes the remote signer reject it with invalid_params.
|
||||||
} else {
|
let ciphertext_b64 = params.get("ciphertext").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
Vec::new()
|
let ciphertext_bytes = match nostr_core::util::base64_decode(ciphertext_b64) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => {
|
||||||
|
respond_error_json(request, 400, &format!("Invalid base64 ciphertext: {}", e));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}).unwrap_or_default();
|
};
|
||||||
match signer.nip44_decrypt(&peer_pk, &ciphertext_bytes) {
|
match signer.nip44_decrypt(&peer_pk, &ciphertext_bytes) {
|
||||||
Ok(plaintext) => {
|
Ok(plaintext) => {
|
||||||
let json = serde_json::json!({ "result": plaintext });
|
let json = serde_json::json!({ "result": plaintext });
|
||||||
@@ -862,8 +892,14 @@ fn extract_query_param(query: &str, key: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Simple URL percent-decoding.
|
/// Simple URL percent-decoding.
|
||||||
|
///
|
||||||
|
/// Decodes into a byte buffer first, then interprets the result as UTF-8.
|
||||||
|
/// This is required for correctness: `encodeURIComponent` percent-encodes
|
||||||
|
/// each byte of a multi-byte UTF-8 sequence separately, so pushing each
|
||||||
|
/// decoded byte as a `char` (the previous behaviour) would corrupt any
|
||||||
|
/// non-ASCII content (e.g. emoji or accented characters in a post body).
|
||||||
fn url_decode(s: &str) -> String {
|
fn url_decode(s: &str) -> String {
|
||||||
let mut result = String::with_capacity(s.len());
|
let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
|
||||||
let mut chars = s.chars().peekable();
|
let mut chars = s.chars().peekable();
|
||||||
while let Some(c) = chars.next() {
|
while let Some(c) = chars.next() {
|
||||||
if c == '%' {
|
if c == '%' {
|
||||||
@@ -871,18 +907,22 @@ fn url_decode(s: &str) -> String {
|
|||||||
let h2 = chars.next();
|
let h2 = chars.next();
|
||||||
if let (Some(a), Some(b)) = (h1, h2) {
|
if let (Some(a), Some(b)) = (h1, h2) {
|
||||||
if let Ok(byte) = u8::from_str_radix(&format!("{}{}", a, b), 16) {
|
if let Ok(byte) = u8::from_str_radix(&format!("{}{}", a, b), 16) {
|
||||||
result.push(byte as char);
|
bytes.push(byte);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.push(c);
|
// Malformed escape — emit the literal '%' and the consumed chars.
|
||||||
|
bytes.push(b'%');
|
||||||
|
if let Some(a) = h1 { bytes.extend_from_slice(a.to_string().as_bytes()); }
|
||||||
|
if let Some(b) = h2 { bytes.extend_from_slice(b.to_string().as_bytes()); }
|
||||||
} else if c == '+' {
|
} else if c == '+' {
|
||||||
result.push(' ');
|
bytes.push(b' ');
|
||||||
} else {
|
} else {
|
||||||
result.push(c);
|
let mut buf = [0u8; 4];
|
||||||
|
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
String::from_utf8_lossy(&bytes).into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a hex pubkey string into a PublicKey.
|
/// Parse a hex pubkey string into a PublicKey.
|
||||||
@@ -895,6 +935,114 @@ fn parse_pubkey_hex(hex_str: &str) -> Option<nostr_core::types::PublicKey> {
|
|||||||
Some(nostr_core::types::PublicKey::from_bytes(arr))
|
Some(nostr_core::types::PublicKey::from_bytes(arr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse an unsigned event JSON body from the JS shim into a
|
||||||
|
/// `nostr_core::types::Event`, injecting the active identity's pubkey.
|
||||||
|
///
|
||||||
|
/// The JS shim sends an *unsigned* event — typically
|
||||||
|
/// `{kind, content, tags, created_at}` with no `pubkey`/`id`/`sig`.
|
||||||
|
/// `nostr_core::types::Event` requires `pubkey` (it is not an `Option`),
|
||||||
|
/// so deserializing the raw body fails with "missing field `pubkey`".
|
||||||
|
/// This fills the pubkey in from the signed-in user, mirroring the C
|
||||||
|
/// bridge. An explicit non-empty `pubkey` in the body is preserved.
|
||||||
|
fn parse_unsigned_event(
|
||||||
|
event_json: &str,
|
||||||
|
pubkey_hex: &str,
|
||||||
|
) -> Result<nostr_core::types::Event, String> {
|
||||||
|
let mut value: serde_json::Value =
|
||||||
|
serde_json::from_str(event_json).map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||||
|
|
||||||
|
if let Some(obj) = value.as_object_mut() {
|
||||||
|
let needs_pubkey = obj
|
||||||
|
.get("pubkey")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.is_empty())
|
||||||
|
.unwrap_or(true);
|
||||||
|
if needs_pubkey {
|
||||||
|
obj.insert(
|
||||||
|
"pubkey".to_string(),
|
||||||
|
serde_json::Value::String(pubkey_hex.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::from_value::<nostr_core::types::Event>(value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const PK: &str = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn injects_pubkey_when_missing() {
|
||||||
|
// Exactly what the JS shim sends for a kind-1 post.
|
||||||
|
let body = r#"{"kind":1,"content":"Hello world","tags":[],"created_at":1700000000}"#;
|
||||||
|
let event = parse_unsigned_event(body, PK).expect("should parse");
|
||||||
|
assert_eq!(event.pubkey.to_hex(), PK);
|
||||||
|
assert_eq!(event.content, "Hello world");
|
||||||
|
assert_eq!(event.kind.as_u64(), 1);
|
||||||
|
assert_eq!(event.created_at, 1700000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_explicit_pubkey() {
|
||||||
|
let other = "0000000000000000000000000000000000000000000000000000000000000001";
|
||||||
|
let body = format!(
|
||||||
|
r#"{{"pubkey":"{}","kind":1,"content":"x","tags":[],"created_at":1}}"#,
|
||||||
|
other
|
||||||
|
);
|
||||||
|
let event = parse_unsigned_event(&body, PK).expect("should parse");
|
||||||
|
assert_eq!(event.pubkey.to_hex(), other);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_tags_and_imeta() {
|
||||||
|
let body = r#"{"kind":1,"content":"hi","tags":[["imeta","url https://example.com/a.png"]],"created_at":5}"#;
|
||||||
|
let event = parse_unsigned_event(body, PK).expect("should parse");
|
||||||
|
assert_eq!(event.tags.len(), 1);
|
||||||
|
assert_eq!(event.tags[0].0[0], "imeta");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_json() {
|
||||||
|
assert!(parse_unsigned_event("not json", PK).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Round-trip the NIP-44 base64 encoding the bridge uses.
|
||||||
|
///
|
||||||
|
/// The JS shim sends the ciphertext as a base64 string; the bridge must
|
||||||
|
/// base64-decode it before handing raw bytes to the signer, and must
|
||||||
|
/// base64-encode the signer's raw output before returning it. This test
|
||||||
|
/// exercises that exact path with two real local signers.
|
||||||
|
#[test]
|
||||||
|
fn nip44_base64_round_trip() {
|
||||||
|
use nostr_signer::local::LocalSigner;
|
||||||
|
use nostr_signer::traits::NostrSigner;
|
||||||
|
|
||||||
|
let (sk_a, pk_a) = nostr_core::crypto::keys::generate_keypair();
|
||||||
|
let (sk_b, pk_b) = nostr_core::crypto::keys::generate_keypair();
|
||||||
|
let signer_a = LocalSigner::new(sk_a).expect("signer a");
|
||||||
|
let signer_b = LocalSigner::new(sk_b).expect("signer b");
|
||||||
|
|
||||||
|
let plaintext = "Hello, giftwrap!";
|
||||||
|
|
||||||
|
// Encrypt as A→B, then base64-encode (what the bridge returns).
|
||||||
|
let raw_ct = signer_a.nip44_encrypt(&pk_b, plaintext).expect("encrypt");
|
||||||
|
let b64_ct = nostr_core::util::base64_encode(&raw_ct);
|
||||||
|
|
||||||
|
// The base64 string must not be valid raw ciphertext bytes — this is
|
||||||
|
// exactly the bug: passing the ASCII of the base64 text to the signer
|
||||||
|
// fails. Decoding first must succeed.
|
||||||
|
let decoded = nostr_core::util::base64_decode(&b64_ct).expect("decode");
|
||||||
|
assert_eq!(decoded, raw_ct);
|
||||||
|
|
||||||
|
// Decrypt as B←A using the decoded raw bytes.
|
||||||
|
let decrypted = signer_b.nip44_decrypt(&pk_a, &decoded).expect("decrypt");
|
||||||
|
assert_eq!(String::from_utf8(decrypted).unwrap(), plaintext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn respond_json(request: &URISchemeRequest, json: &str) {
|
fn respond_json(request: &URISchemeRequest, json: &str) {
|
||||||
let body = json.to_string().into_bytes();
|
let body = json.to_string().into_bytes();
|
||||||
let stream = MemoryInputStream::from_bytes(&Bytes::from_owned(body.clone()));
|
let stream = MemoryInputStream::from_bytes(&Bytes::from_owned(body.clone()));
|
||||||
|
|||||||
+6
-3
@@ -188,19 +188,22 @@ pub fn nostr_inject_create_content_manager_with_probe(tab_index: i32) -> UserCon
|
|||||||
manager.add_script(&script);
|
manager.add_script(&script);
|
||||||
|
|
||||||
// Perf probe preamble (skips sovereign:// pages, sets tab index).
|
// Perf probe preamble (skips sovereign:// pages, sets tab index).
|
||||||
|
// Top frame only — mirrors the C version (perf_probe.c:67). Injecting
|
||||||
|
// into all frames would run a separate probe instance (with its own
|
||||||
|
// rAF loop and 1s sync XHR) in every iframe.
|
||||||
let preamble = UserScript::new(
|
let preamble = UserScript::new(
|
||||||
&perf_probe_preamble(tab_index),
|
&perf_probe_preamble(tab_index),
|
||||||
UserContentInjectedFrames::AllFrames,
|
UserContentInjectedFrames::TopFrame,
|
||||||
UserScriptInjectionTime::Start,
|
UserScriptInjectionTime::Start,
|
||||||
&[],
|
&[],
|
||||||
&[],
|
&[],
|
||||||
);
|
);
|
||||||
manager.add_script(&preamble);
|
manager.add_script(&preamble);
|
||||||
|
|
||||||
// Perf probe body.
|
// Perf probe body (top frame only — see above).
|
||||||
let probe = UserScript::new(
|
let probe = UserScript::new(
|
||||||
PERF_PROBE_JS,
|
PERF_PROBE_JS,
|
||||||
UserContentInjectedFrames::AllFrames,
|
UserContentInjectedFrames::TopFrame,
|
||||||
UserScriptInjectionTime::Start,
|
UserScriptInjectionTime::Start,
|
||||||
&[],
|
&[],
|
||||||
&[],
|
&[],
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ pub struct BrowserSettings {
|
|||||||
pub enable_smooth_scrolling: bool,
|
pub enable_smooth_scrolling: bool,
|
||||||
pub enable_accelerated_2d_canvas: bool,
|
pub enable_accelerated_2d_canvas: bool,
|
||||||
pub enable_write_console_messages_to_stdout: bool,
|
pub enable_write_console_messages_to_stdout: bool,
|
||||||
|
/// Per-tab performance probe injection. OFF by default: the probe
|
||||||
|
/// patches addEventListener/setTimeout/setInterval/XHR and runs a
|
||||||
|
/// permanent rAF loop, which adds enough main-thread overhead to
|
||||||
|
/// break scrolling on listener-heavy pages. Mirrors the C default
|
||||||
|
/// (settings.c:136).
|
||||||
|
pub perf_probe_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BrowserSettings {
|
impl Default for BrowserSettings {
|
||||||
@@ -69,7 +75,12 @@ impl Default for BrowserSettings {
|
|||||||
enable_media_source: true,
|
enable_media_source: true,
|
||||||
enable_smooth_scrolling: true,
|
enable_smooth_scrolling: true,
|
||||||
enable_accelerated_2d_canvas: true,
|
enable_accelerated_2d_canvas: true,
|
||||||
|
// Console-to-stdout is OFF by default: the browser's stdout is
|
||||||
|
// usually a terminal, where writes are slow and can block the
|
||||||
|
// WebProcess. Enable only when debugging.
|
||||||
enable_write_console_messages_to_stdout: false,
|
enable_write_console_messages_to_stdout: false,
|
||||||
|
// Perf probe OFF by default (see field docs).
|
||||||
|
perf_probe_enabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +133,12 @@ pub fn settings_update(json: &serde_json::Value) {
|
|||||||
if let Some(v) = json.get("theme_dark").and_then(|v| v.as_bool()) {
|
if let Some(v) = json.get("theme_dark").and_then(|v| v.as_bool()) {
|
||||||
settings.theme_dark = v;
|
settings.theme_dark = v;
|
||||||
}
|
}
|
||||||
|
if let Some(v) = json.get("perf_probe_enabled").and_then(|v| v.as_bool()) {
|
||||||
|
settings.perf_probe_enabled = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = json.get("enable_write_console_messages_to_stdout").and_then(|v| v.as_bool()) {
|
||||||
|
settings.enable_write_console_messages_to_stdout = v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load settings from the database.
|
/// Load settings from the database.
|
||||||
@@ -149,6 +166,8 @@ pub fn settings_save() {
|
|||||||
"agent_port": settings.agent_port,
|
"agent_port": settings.agent_port,
|
||||||
"session_restore": settings.session_restore,
|
"session_restore": settings.session_restore,
|
||||||
"theme_dark": settings.theme_dark,
|
"theme_dark": settings.theme_dark,
|
||||||
|
"perf_probe_enabled": settings.perf_probe_enabled,
|
||||||
|
"enable_write_console_messages_to_stdout": settings.enable_write_console_messages_to_stdout,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(json_str) = serde_json::to_string(&json) {
|
if let Ok(json_str) = serde_json::to_string(&json) {
|
||||||
|
|||||||
+10
-2
@@ -218,8 +218,16 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti
|
|||||||
state.active_tab_id = tab_id;
|
state.active_tab_id = tab_id;
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
// Create the webview with perf probe injected (tab index baked into preamble).
|
// Create the webview. The perf probe is injected only when explicitly
|
||||||
let content_manager = crate::nostr_inject::nostr_inject_create_content_manager_with_probe(tab_id);
|
// enabled: it patches addEventListener/setTimeout/setInterval/XHR and
|
||||||
|
// runs a permanent rAF loop, which adds enough main-thread overhead to
|
||||||
|
// break scrolling on listener-heavy pages. Mirrors the C gate
|
||||||
|
// (tab_manager.c:1349). Default OFF.
|
||||||
|
let content_manager = if s.perf_probe_enabled {
|
||||||
|
crate::nostr_inject::nostr_inject_create_content_manager_with_probe(tab_id)
|
||||||
|
} else {
|
||||||
|
crate::nostr_inject::nostr_inject_create_content_manager()
|
||||||
|
};
|
||||||
let webview = WebView::builder()
|
let webview = WebView::builder()
|
||||||
.user_content_manager(&content_manager)
|
.user_content_manager(&content_manager)
|
||||||
.web_context(ctx)
|
.web_context(ctx)
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
//! Version information for sovereign_browser
|
//! Version information for sovereign_browser
|
||||||
|
|
||||||
/// The current version of sovereign_browser (with leading 'v').
|
/// The current version of sovereign_browser (with leading 'v').
|
||||||
pub const VERSION: &str = "v0.0.6";
|
pub const VERSION: &str = "v0.0.7";
|
||||||
|
|
||||||
/// Major version number.
|
/// Major version number.
|
||||||
pub const VERSION_MAJOR: u32 = 0;
|
pub const VERSION_MAJOR: u32 = 0;
|
||||||
@@ -10,4 +10,4 @@ pub const VERSION_MAJOR: u32 = 0;
|
|||||||
pub const VERSION_MINOR: u32 = 0;
|
pub const VERSION_MINOR: u32 = 0;
|
||||||
|
|
||||||
/// Patch version number.
|
/// Patch version number.
|
||||||
pub const VERSION_PATCH: u32 = 6;
|
pub const VERSION_PATCH: u32 = 7;
|
||||||
|
|||||||
+7
-4
@@ -50,8 +50,11 @@ pub fn web_context_configure_settings(settings: &webkit2gtk::Settings) {
|
|||||||
// Deprecated since 2.32 with no replacement; keep for explicitness.
|
// Deprecated since 2.32 with no replacement; keep for explicitness.
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
settings.set_enable_accelerated_2d_canvas(true);
|
settings.set_enable_accelerated_2d_canvas(true);
|
||||||
// Enable console messages to stdout so we can debug window.nostr injection
|
// Console-to-stdout is OFF by default. The browser's stdout is usually a
|
||||||
// and page-level issues. The C version connects to console-message signal;
|
// terminal, where writes are slow and can block the WebProcess; on a
|
||||||
// this is simpler for now.
|
// chatty page this is a real CPU cost. Enable via the
|
||||||
settings.set_enable_write_console_messages_to_stdout(true);
|
// `enable_write_console_messages_to_stdout` setting when debugging.
|
||||||
|
settings.set_enable_write_console_messages_to_stdout(
|
||||||
|
crate::settings::settings_get().enable_write_console_messages_to_stdout,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,17 @@
|
|||||||
onclick="toggle('universal_access')"></div>
|
onclick="toggle('universal_access')"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting">
|
||||||
|
<div>
|
||||||
|
<div class="setting-name">Performance Probe</div>
|
||||||
|
<div class="setting-desc">Per-tab CPU/FPS instrumentation. Adds main-thread
|
||||||
|
overhead (patches timers, listeners, XHR; runs a rAF loop). Off by
|
||||||
|
default. Takes effect on new tabs.</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle" data-feature="perf_probe_enabled"
|
||||||
|
onclick="toggle('perf_probe_enabled')"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<div><div class="setting-name">CORS Enforcement</div></div>
|
<div><div class="setting-name">CORS Enforcement</div></div>
|
||||||
<div class="toggle off" style="opacity:0.5"></div>
|
<div class="toggle off" style="opacity:0.5"></div>
|
||||||
|
|||||||
+2
-1
@@ -49,7 +49,8 @@ function applyConfig(d) {
|
|||||||
agent_server_enabled: d.agent_server_enabled,
|
agent_server_enabled: d.agent_server_enabled,
|
||||||
dev_extras: d.dev_extras,
|
dev_extras: d.dev_extras,
|
||||||
file_access: d.file_access,
|
file_access: d.file_access,
|
||||||
universal_access: d.universal_access
|
universal_access: d.universal_access,
|
||||||
|
perf_probe_enabled: d.perf_probe_enabled
|
||||||
};
|
};
|
||||||
document.querySelectorAll('.toggle[data-feature]').forEach(function(el) {
|
document.querySelectorAll('.toggle[data-feature]').forEach(function(el) {
|
||||||
var key = el.getAttribute('data-feature');
|
var key = el.getAttribute('data-feature');
|
||||||
|
|||||||
Reference in New Issue
Block a user