using Jint; using Jint.Native; using Jint.Native.Object; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; namespace XFESystem.Browser; internal sealed class BrowserPerformance { private sealed record Entry(string Name, string Type, double Start, double Duration, JsValue Detail); private readonly Engine engine; private readonly BrowserLimits limits; private readonly Func clock; private readonly List entries = []; private long droppedEntries; public BrowserPerformance(Engine engine, BrowserLimits limits, Func clock) { this.engine = engine; this.limits = limits; this.clock = clock; var target = new BrowserHostObject(engine); Data(target, "timeOrigin", (double)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); target.DefineOwnProperty("droppedEntriesCount", new GetSetPropertyDescriptor( new ClrFunction(engine, "get droppedEntriesCount", (_, _) => (double)droppedEntries), null, true, false)); Function(target, "now", _ => (double)clock()); Function(target, "mark", args => { var options = Value(args, 1); var start = options.IsObject() && options.AsObject().Get("startTime").IsNumber() ? options.AsObject().Get("startTime").AsNumber() : clock(); if (!double.IsFinite(start) || start < 0) throw new ArgumentException("Invalid performance mark time."); return Add(new(Name(Value(args, 0)), "mark", start, 0, options.IsObject() ? options.AsObject().Get("detail") : JsValue.Null)); }); Function(target, "measure", args => { double start = 0, end = clock(); var options = Value(args, 1); JsValue detail = JsValue.Null; if (options.IsObject()) { var settings = options.AsObject(); var from = settings.Get("start"); var to = settings.Get("end"); var duration = settings.Get("duration"); detail = settings.Get("detail"); if (!from.IsUndefined()) start = Time(from); if (!to.IsUndefined()) end = Time(to); if (duration.IsNumber()) { if (!to.IsUndefined() && from.IsUndefined()) start = end - duration.AsNumber(); else end = start + duration.AsNumber(); } } else { if (!options.IsUndefined()) start = Time(options); if (!Value(args, 2).IsUndefined()) end = Time(Value(args, 2)); } if (!double.IsFinite(start + end) || start < 0 || end < start) throw new ArgumentException("Invalid performance measure time."); return Add(new(Name(Value(args, 0)), "measure", start, end - start, detail)); }); Function(target, "clearMarks", args => { Clear("mark", Value(args, 0)); return JsValue.Undefined; }); Function(target, "clearMeasures", args => { Clear("measure", Value(args, 0)); return JsValue.Undefined; }); Function(target, "getEntries", _ => Array(entries)); Function(target, "getEntriesByName", args => { var name = Name(Value(args, 0)); var type = Value(args, 1); return Array(entries.Where(e => e.Name == name && (type.IsUndefined() || e.Type == Name(type)))); }); Function(target, "getEntriesByType", args => { var type = Name(Value(args, 0)); return Array(entries.Where(e => e.Type == type)); }); engine.SetValue("performance", target); } private JsValue Add(Entry entry) { if (entries.Count >= limits.MaximumPendingTasks) { entries.RemoveAt(0); ++droppedEntries; } entries.Add(entry); return Project(entry); } private ObjectInstance Project(Entry entry) { var value = new BrowserHostObject(engine); Data(value, "name", entry.Name); Data(value, "entryType", entry.Type); Data(value, "startTime", entry.Start); Data(value, "duration", entry.Duration); Data(value, "detail", entry.Detail); Function(value, "toJSON", _ => value); return value; } private JsValue Array(IEnumerable values) => new JsArray(engine, values.OrderBy(e => e.Start).Select(e => (JsValue)Project(e)).ToArray()); private double Time(JsValue value) { if (value.IsNumber()) return value.AsNumber(); var name = Name(value); return entries.LastOrDefault(e => e.Type == "mark" && e.Name == name)?.Start ?? throw new ArgumentException("The performance mark does not exist: " + name); } private void Clear(string type, JsValue name) { if (name.IsUndefined()) entries.RemoveAll(e => e.Type == type); else { var text = Name(name); entries.RemoveAll(e => e.Type == type && e.Name == text); } } private static string Name(JsValue value) { if (value.IsObject()) throw new ArgumentException("Performance names must be primitive values."); var name = value.ToString(); if (name.Length > 1024) throw new BrowserLimitException("The performance entry name is too long."); return name; } private void Function(ObjectInstance value, string name, Func callback) => value.DefineOwnProperty(name, new PropertyDescriptor(new ClrFunction(engine, name, (_, args) => callback(args)), false, true, false)); private static void Data(ObjectInstance value, string name, JsValue content) => value.DefineOwnProperty(name, new PropertyDescriptor(content, false, true, false)); private static JsValue Value(JsValue[] args, int index) => index < args.Length ? args[index] : JsValue.Undefined; }