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<long> clock;
private readonly List<Entry> entries = [];
private long droppedEntries;
public BrowserPerformance(Engine engine, BrowserLimits limits, Func<long> 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<Entry> 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<JsValue[], JsValue> 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;
}
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<long> clock;
private readonly List<Entry> entries = [];
private long droppedEntries;
public BrowserPerformance(Engine engine, BrowserLimits limits, Func<long> 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<Entry> 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<JsValue[], JsValue> 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;
}