// FR-29 — the field readiness curve and field level early stop.
//
// The "95% coverage" figure of the first evaluation round was structurally
// insensitive (an object is either visible or it is not), and "the first
// visible value arrives after 0.7% of the document" says nothing about *which*
// field becomes usable when. Both are replaced by one measurement:
//
// for every top-level member, the first input fraction at which the value it
// holds is already equal to the value it ends up with.
//
// A consumer that waits for the whole document has every field ready at 1.000,
// so the curve of that baseline is flat at 0 until the end. A consumer that
// only needs the fields it saw so far can cancel the generation as soon as the
// last one it needs is ready, which is the saving this file reports.
///|
/// Readiness point of one top-level member.
pub(all) struct FieldReady {
/// Member name.
key : String
/// First input fraction, in per mille of the whole document, at which the
/// member's value was already finmap. `1000` means "not before the end".
permille : Int32
}
///|
/// Explicit `Eq` implementation for `FieldReady`.
pub impl Eq for FieldReady with fn equal(self : FieldReady, other : FieldReady) -> Bool {
self.key == other.key && self.permille == other.permille
}
///|
/// Explicit `Eq` extension for `FieldReady`.
pub extend FieldReady with Eq::{not_equal, equal}
///|
/// Readiness curve of one document (FR-29).
///
/// `step` is the number of code units fed between two observations; `0` (the
/// default) picks a step that samples the document about a thousand times, so
/// the result does not depend on the document length.
///
/// Since 0.6.0 this listens to the delta stream instead of re-reading the
/// document: a top-level member is ready at the offset of its own final delta,
/// which is one pass with one drain per step rather than one partial parse *and
/// one snapshot* per step (FR-39). On a 100 KiB payload that is ~30x.
///
/// A document whose root is not an object has no fields and yields `[]`.
pub fn field_readiness(
input : String,
step? : Int32,
options? : Options,
) -> Array[FieldReady] raise MendError {
let n = input.length()
let fin = parse(input, options?)
let finmap = match fin.value {
Json::Object(m) => m
_ => return []
}
let st = auto_step(
n,
match step {
Some(v) => v
None => 0
},
)
// The final member list is what the report is about; each one is followed
// with an `any` declaration, because FR-29 asks *when the value was final*,
// not whether it had the declared kind.
let members : Array[String] = []
let specs : Array[SchemaMember] = []
for k, _v in finmap {
members.push(k)
specs.push({ key: k, required: true, schema: Any, })
}
let skeleton = Obj(specs)
let events = watch_readiness(input, skeleton, chunk=st, options?)
let seen : Map[String, Int32] = Map([])
for e in events {
let key = e.event.path()
if key == "" || seen.contains(key) {
continue
}
match e.event {
FieldReady(_) | FieldInvalid(_, _) =>
// `-1` means the member was only settled by `finish()`: "not before the
// end".
seen[key] = if e.permille < 0 { 1000 } else { e.permille }
_ => ()
}
}
let out : Array[FieldReady] = []
for k in members {
match seen.get(k) {
Some(p) => out.push({ key: k, permille: p, })
None => out.push({ key: k, permille: 1000, })
}
}
out
}
///|
/// Median readiness point, in per mille, over every field of every document
/// (FR-29). Documents without fields are ignored.
pub fn readiness_median(
docs : Array[String],
step? : Int32,
) -> Int32 raise MendError {
let all : Array[Int32] = []
for text in docs {
for f in field_readiness(text, step?) {
all.push(f.permille)
}
}
if all.length() == 0 {
return 1000
}
all.sort()
all[all.length() / 2]
}
///|
/// The readiness curve over `docs`, as eleven percentages: entry `d` is the
/// share (0…100) of fields whose value was already finmap when `d` tenths of
/// the input had been consumed (FR-29). Entry 0 is 0 by construction, entry 10
/// is 100.
pub fn readiness_deciles(
docs : Array[String],
step? : Int32,
) -> Array[Int32] raise MendError {
let points : Array[Int32] = []
for text in docs {
for f in field_readiness(text, step?) {
points.push(f.permille)
}
}
let out : Array[Int32] = []
for d = 0; d <= 10; d = d + 1 {
let limit = d * 100
let mut hit = 0
for p in points {
if p <= limit {
hit = hit + 1
}
}
if points.length() == 0 {
out.push(0)
} else {
out.push(hit * 100 / points.length())
}
}
out
}
///|
/// Average share of the input, in per mille, that a consumer never has to read
/// if it stops the generation as soon as every field of a document is ready
/// (FR-29).
///
/// The baseline is 0 by definition: waiting for the end of the document reads
/// all of it. A document whose root is not an object contributes 0.
pub fn early_stop_saving(
docs : Array[String],
step? : Int32,
) -> Int32 raise MendError {
if docs.length() == 0 {
return 0
}
let mut total = 0
for text in docs {
let fields = field_readiness(text, step?)
let mut last = 0
for f in fields {
if f.permille > last {
last = f.permille
}
}
if fields.length() > 0 {
total = total + (1000 - last)
}
}
total / docs.length()
}
// ---------------------------------------------------------------------------
// The runtime half of the same idea: watch a delta stream instead of a text.
// ---------------------------------------------------------------------------
///|
/// Watches a set of top-level members of the object a model is writing and
/// reports when each one can no longer change, so the caller can cancel the
/// generation (FR-29).
///
/// The rule is read off the delta contract rather than guessed:
///
/// * `Close` on `/` — a container member is finmap;
/// * `Set` on `/` with a completed scalar — a number, literal, or a
/// non-empty string is finmap (an empty string is the placeholder that
/// announces a string slot, followed by `AppendText`);
/// * any later delta about a different first-level member — the member before
/// it cannot change any more.
pub struct FieldTracker {
priv watched : Array[String]
priv seen : Array[Bool]
priv ready : Array[Bool]
}
///|
/// A tracker for `keys`.
pub fn FieldTracker::new(keys : Array[String]) -> FieldTracker {
let seen : Array[Bool] = []
let ready : Array[Bool] = []
for _k in keys {
seen.push(false)
ready.push(false)
}
{ watched: keys.copy(), seen, ready, }
}
///|
/// First-level member a delta talks about, or "".
fn delta_top_key(d : Delta) -> String {
if d.path.length() == 0 {
""
} else {
match d.path[0] {
Key(k) => k
Index(_) => ""
}
}
}
///|
/// Whether a `Set` carries a scalar that is already finmap.
fn settled_scalar(d : Delta) -> Bool {
match d.value {
Some(Json::String(s)) => s != ""
Some(Json::Array(_)) | Some(Json::Object(_)) => false
Some(_) => true
None => false
}
}
///|
/// Feed one delta.
pub fn FieldTracker::feed(self : FieldTracker, d : Delta) -> Unit {
let top = delta_top_key(d)
for i = 0; i < self.watched.length(); i = i + 1 {
if self.seen[i] && !self.ready[i] && top != "" && top != self.watched[i] { // another member started
self.ready[i] = true
}
if top == self.watched[i] && top != "" {
self.seen[i] = true
if d.path.length() == 1 {
let settled = match d.op {
Close => true
Set => settled_scalar(d)
AppendText => false
}
if settled {
self.ready[i] = true
}
}
}
}
}
///|
/// Whether every watched member is finmap.
pub fn FieldTracker::all_ready(self : FieldTracker) -> Bool {
for i = 0; i < self.watched.length(); i = i + 1 {
if !self.ready[i] {
return false
}
}
true
}
///|
/// Whether the named member is finmap.
pub fn FieldTracker::ready(self : FieldTracker, key : String) -> Bool {
for i = 0; i < self.watched.length(); i = i + 1 {
if self.watched[i] == key {
return self.ready[i]
}
}
false
}
///|
/// How many watched members are finmap.
pub fn FieldTracker::ready_count(self : FieldTracker) -> Int32 {
let mut n = 0
for i = 0; i < self.watched.length(); i = i + 1 {
if self.ready[i] {
n = n + 1
}
}
n
}
///|
/// Names of the watched members that are finmap, in the order they were ready.
pub fn FieldTracker::ready_keys(self : FieldTracker) -> Array[String] {
let out : Array[String] = []
for i = 0; i < self.watched.length(); i = i + 1 {
if self.ready[i] {
out.push(self.watched[i])
}
}
out
}