@mentra/crust 3.3.0-dev.598 → 3.3.0-dev.600
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +2 -1
- package/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt +52 -4
- package/android/src/main/java/com/mentra/crust/jsc/JSDeliveryDiagnostics.kt +44 -0
- package/android/src/test/java/com/mentra/crust/jsc/JSDeliveryDiagnosticsTest.kt +54 -0
- package/package.json +2 -2
|
@@ -308,7 +308,8 @@ class CrustModule : Module() {
|
|
|
308
308
|
?: appContext.currentActivity
|
|
309
309
|
?: throw IllegalStateException("MentraJS: no context")
|
|
310
310
|
val json = org.json.JSONObject(envelope as Map<*, *>).toString()
|
|
311
|
-
|
|
311
|
+
val deliveryId = (envelope["deliveryId"] as? Number)?.toLong()?.takeIf { it > 0 }
|
|
312
|
+
JSCRuntime.shared(ctx).dispatchToJs(packageName, json, deliveryId)
|
|
312
313
|
}.runOnQueue(mentraJsQueue)
|
|
313
314
|
|
|
314
315
|
AsyncFunction("mentraJsSetManifest") { packageName: String, permissions: List<String> ->
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.mentra.crust.jsc
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.os.SystemClock
|
|
4
5
|
import android.util.Log
|
|
5
6
|
import com.dokar.quickjs.QuickJs
|
|
6
7
|
import com.dokar.quickjs.binding.function
|
|
@@ -11,6 +12,7 @@ import java.util.concurrent.Executors
|
|
|
11
12
|
import java.util.concurrent.ScheduledExecutorService
|
|
12
13
|
import java.util.concurrent.ScheduledFuture
|
|
13
14
|
import java.util.concurrent.TimeUnit
|
|
15
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
14
16
|
import kotlinx.coroutines.Dispatchers
|
|
15
17
|
import kotlinx.coroutines.runBlocking
|
|
16
18
|
|
|
@@ -133,6 +135,7 @@ class JSCRuntime private constructor(private val appContext: Context) {
|
|
|
133
135
|
val dispatcher: JSCDispatcher = JSCDispatcher(appContext)
|
|
134
136
|
|
|
135
137
|
private val contexts = ConcurrentHashMap<String, ContextRecord>()
|
|
138
|
+
private val nextContextId = AtomicLong()
|
|
136
139
|
private val polyfillBundle: String by lazy { loadPolyfillBundle() }
|
|
137
140
|
|
|
138
141
|
// Schedule pool for timers; per-context the executor is the same as the
|
|
@@ -146,6 +149,7 @@ class JSCRuntime private constructor(private val appContext: Context) {
|
|
|
146
149
|
val packageName: String,
|
|
147
150
|
val qjs: QuickJs,
|
|
148
151
|
val executor: ExecutorService,
|
|
152
|
+
val contextId: Long,
|
|
149
153
|
val pendingTimers: MutableMap<Int, ScheduledFuture<*>> = ConcurrentHashMap(),
|
|
150
154
|
@Volatile var readyAcked: Boolean = false,
|
|
151
155
|
@Volatile var readyNackTimer: ScheduledFuture<*>? = null,
|
|
@@ -273,6 +277,7 @@ class JSCRuntime private constructor(private val appContext: Context) {
|
|
|
273
277
|
packageName = packageName,
|
|
274
278
|
qjs = qjs,
|
|
275
279
|
executor = executor,
|
|
280
|
+
contextId = nextContextId.incrementAndGet(),
|
|
276
281
|
)
|
|
277
282
|
contexts[packageName] = record
|
|
278
283
|
|
|
@@ -457,10 +462,23 @@ class JSCRuntime private constructor(private val appContext: Context) {
|
|
|
457
462
|
* Push a `{kind: "event"|"response", …}` envelope into the named
|
|
458
463
|
* context's globalThis.__deliver. Hops onto the per-context executor.
|
|
459
464
|
*/
|
|
460
|
-
fun dispatchToJs(packageName: String, envelopeJson: String) {
|
|
461
|
-
val record = contexts[packageName]
|
|
465
|
+
fun dispatchToJs(packageName: String, envelopeJson: String, deliveryId: Long? = null) {
|
|
466
|
+
val record = contexts[packageName]
|
|
467
|
+
// Only host-generated PINGs carry this ID. Ordinary media/event deliveries stay quiet.
|
|
468
|
+
val diagnostic = deliveryId?.let {
|
|
469
|
+
JSDeliveryDiagnostics(it, record?.contextId, SystemClock::elapsedRealtime) { metadata ->
|
|
470
|
+
reportDeliveryDiagnostic(packageName, metadata)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
diagnostic?.entry()
|
|
474
|
+
if (record == null) {
|
|
475
|
+
diagnostic?.dropped("context-missing")
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
diagnostic?.queued()
|
|
462
479
|
try {
|
|
463
480
|
record.executor.submit {
|
|
481
|
+
diagnostic?.started()
|
|
464
482
|
// Arm only once evaluate is about to run. Queue wait during a
|
|
465
483
|
// long previous turn (WHIP start, etc.) must not count — that
|
|
466
484
|
// was firing ready_nack while the context was merely busy.
|
|
@@ -489,14 +507,26 @@ class JSCRuntime private constructor(private val appContext: Context) {
|
|
|
489
507
|
kill(packageName)
|
|
490
508
|
}, WATCHDOG_KILL_MS, TimeUnit.MILLISECONDS)
|
|
491
509
|
record.watchdogTimer = killTimer
|
|
510
|
+
var failure: Throwable? = null
|
|
492
511
|
try {
|
|
493
512
|
val source = "globalThis.__deliver(${jsStringLiteral(envelopeJson)});"
|
|
494
513
|
runBlocking {
|
|
495
514
|
record.qjs.evaluate<Any?>(source, filename = "mentrajs:deliver.js")
|
|
496
515
|
}
|
|
497
516
|
} catch (e: Throwable) {
|
|
517
|
+
failure = e
|
|
498
518
|
Log.w(TAG, "dispatchToJs threw in $packageName: ${e.message}", e)
|
|
519
|
+
if (diagnostic == null) {
|
|
520
|
+
reportDeliveryDiagnostic(packageName, mapOf(
|
|
521
|
+
"event" to "native-delivery-failed",
|
|
522
|
+
"contextId" to record.contextId,
|
|
523
|
+
"phase" to "evaluate-failed",
|
|
524
|
+
"errorClass" to e.javaClass.simpleName,
|
|
525
|
+
"causeClass" to e.cause?.javaClass?.simpleName,
|
|
526
|
+
))
|
|
527
|
+
}
|
|
499
528
|
} finally {
|
|
529
|
+
diagnostic?.finished(failure)
|
|
500
530
|
// Success or throw: the host observed the turn end.
|
|
501
531
|
record.readyNackTimer?.cancel(false)
|
|
502
532
|
record.readyNackTimer = null
|
|
@@ -505,8 +535,26 @@ class JSCRuntime private constructor(private val appContext: Context) {
|
|
|
505
535
|
if (record.watchdogTimer === killTimer) record.watchdogTimer = null
|
|
506
536
|
}
|
|
507
537
|
}
|
|
508
|
-
} catch (
|
|
509
|
-
|
|
538
|
+
} catch (e: java.util.concurrent.RejectedExecutionException) {
|
|
539
|
+
diagnostic?.dropped("executor-rejected", e)
|
|
540
|
+
Log.w(TAG, "dispatchToJs rejected for $packageName", e)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private fun reportDeliveryDiagnostic(packageName: String, metadata: Map<String, Any?>) {
|
|
545
|
+
val line = org.json.JSONObject(metadata as Map<*, *>).toString()
|
|
546
|
+
Log.i(TAG, line)
|
|
547
|
+
// Use the existing miniapp log route so console interception includes this in reports.
|
|
548
|
+
try {
|
|
549
|
+
deliverOrDrop(OutboundMessage(packageName, mapOf(
|
|
550
|
+
"packageName" to packageName,
|
|
551
|
+
"iface" to "__log",
|
|
552
|
+
"method" to if (metadata["errorClass"] == null) "log" else "warn",
|
|
553
|
+
"argsJson" to org.json.JSONArray().put(org.json.JSONObject(metadata as Map<*, *>)).toString(),
|
|
554
|
+
)))
|
|
555
|
+
} catch (e: Throwable) {
|
|
556
|
+
// Observability cannot change delivery or hide its original failure.
|
|
557
|
+
Log.w(TAG, "delivery diagnostic forwarding failed: ${e.javaClass.simpleName}")
|
|
510
558
|
}
|
|
511
559
|
}
|
|
512
560
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package com.mentra.crust.jsc
|
|
2
|
+
|
|
3
|
+
/** Metadata for one host PING. Never retains the envelope or exception message. */
|
|
4
|
+
internal class JSDeliveryDiagnostics(
|
|
5
|
+
private val deliveryId: Long,
|
|
6
|
+
private val contextId: Long?,
|
|
7
|
+
private val clockMs: () -> Long,
|
|
8
|
+
private val emit: (Map<String, Any?>) -> Unit,
|
|
9
|
+
) {
|
|
10
|
+
private val enteredAtMs = clockMs()
|
|
11
|
+
private var queuedAtMs: Long? = null
|
|
12
|
+
private var startedAtMs: Long? = null
|
|
13
|
+
|
|
14
|
+
fun entry() = report("native-entry")
|
|
15
|
+
|
|
16
|
+
fun queued() {
|
|
17
|
+
queuedAtMs = clockMs()
|
|
18
|
+
report("queued")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun started() {
|
|
22
|
+
startedAtMs = clockMs()
|
|
23
|
+
report("started")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fun finished(error: Throwable? = null) = report(if (error == null) "finished" else "evaluate-failed", error)
|
|
27
|
+
|
|
28
|
+
fun dropped(reason: String, error: Throwable? = null) = report(reason, error)
|
|
29
|
+
|
|
30
|
+
private fun report(phase: String, error: Throwable? = null) {
|
|
31
|
+
val now = clockMs()
|
|
32
|
+
emit(mapOf(
|
|
33
|
+
"event" to "native-ping-delivery",
|
|
34
|
+
"deliveryId" to deliveryId,
|
|
35
|
+
"contextId" to contextId,
|
|
36
|
+
"phase" to phase,
|
|
37
|
+
"elapsedMs" to now - enteredAtMs,
|
|
38
|
+
"queueWaitMs" to queuedAtMs?.let { (startedAtMs ?: now) - it },
|
|
39
|
+
"evaluateMs" to startedAtMs?.let { now - it },
|
|
40
|
+
"errorClass" to error?.javaClass?.simpleName,
|
|
41
|
+
"causeClass" to error?.cause?.javaClass?.simpleName,
|
|
42
|
+
))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package com.mentra.crust.jsc
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class JSDeliveryDiagnosticsTest {
|
|
7
|
+
@Test fun `queue wait and evaluate duration are distinct`() {
|
|
8
|
+
var now = 100L
|
|
9
|
+
val events = mutableListOf<Map<String, Any?>>()
|
|
10
|
+
val diagnostic = JSDeliveryDiagnostics(7, 2, { now }, { events += it })
|
|
11
|
+
diagnostic.entry()
|
|
12
|
+
now = 110
|
|
13
|
+
diagnostic.queued()
|
|
14
|
+
now = 310
|
|
15
|
+
diagnostic.started()
|
|
16
|
+
now = 350
|
|
17
|
+
diagnostic.finished()
|
|
18
|
+
|
|
19
|
+
assertEquals(listOf("native-entry", "queued", "started", "finished"), events.map { it["phase"] })
|
|
20
|
+
assertEquals(200L, events.last()["queueWaitMs"])
|
|
21
|
+
assertEquals(40L, events.last()["evaluateMs"])
|
|
22
|
+
assertEquals(250L, events.last()["elapsedMs"])
|
|
23
|
+
assertEquals(7L, events.last()["deliveryId"])
|
|
24
|
+
assertEquals(2L, events.last()["contextId"])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test fun `failure includes exception identity without payload or message`() {
|
|
28
|
+
val events = mutableListOf<Map<String, Any?>>()
|
|
29
|
+
val diagnostic = JSDeliveryDiagnostics(1, 4, { 0L }, { events += it })
|
|
30
|
+
val error = IllegalStateException("private-token user transcript", IllegalArgumentException("private-source"))
|
|
31
|
+
diagnostic.entry()
|
|
32
|
+
diagnostic.queued()
|
|
33
|
+
diagnostic.started()
|
|
34
|
+
diagnostic.finished(error)
|
|
35
|
+
|
|
36
|
+
assertEquals("evaluate-failed", events.last()["phase"])
|
|
37
|
+
assertEquals("IllegalStateException", events.last()["errorClass"])
|
|
38
|
+
assertEquals("IllegalArgumentException", events.last()["causeClass"])
|
|
39
|
+
assertFalse(events.toString().contains("private-"))
|
|
40
|
+
assertFalse(events.toString().contains("transcript"))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test fun `missing context and rejected executor have explicit terminal events`() {
|
|
44
|
+
for (reason in listOf("context-missing", "executor-rejected")) {
|
|
45
|
+
val events = mutableListOf<Map<String, Any?>>()
|
|
46
|
+
val diagnostic = JSDeliveryDiagnostics(9, null, { 0L }, { events += it })
|
|
47
|
+
diagnostic.entry()
|
|
48
|
+
diagnostic.dropped(reason)
|
|
49
|
+
assertEquals(reason, events.last()["phase"])
|
|
50
|
+
assertNull(events.last()["contextId"])
|
|
51
|
+
assertNull(events.last()["evaluateMs"])
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/crust",
|
|
3
|
-
"version": "3.3.0-dev.
|
|
3
|
+
"version": "3.3.0-dev.600",
|
|
4
4
|
"description": "Mentra Native Module",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"license": "Apache-2.0",
|
|
36
36
|
"homepage": "https://github.com/Mentra-Community/MentraOS/tree/dev/mobile/modules/crust#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@mentra/jspolyfill": "3.3.0-dev.
|
|
38
|
+
"@mentra/jspolyfill": "3.3.0-dev.600"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@expo/config-plugins": ">=8.0.0",
|