ntk 8.11.0 → 8.12.0

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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/lib/window.js +250 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -106,7 +106,8 @@ wnd.requestAnimationFrame(frame);
106
106
  ```
107
107
 
108
108
  See [docs/window.md](docs/window.md) for the knobs (`frameInterval`,
109
- `frameSync`, `coalesceEvents`) and the raw uncoalesced event stream.
109
+ `frameSync`, `maxFramesInFlight`, `coalesceEvents`) and the raw uncoalesced
110
+ event stream.
110
111
 
111
112
  ## Resource management
112
113
 
package/lib/window.js CHANGED
@@ -60,6 +60,43 @@ const BACKING_GRANULARITY = 128;
60
60
  */
61
61
  const DEFAULT_FRAME_INTERVAL = 16;
62
62
 
63
+ /**
64
+ * How many frames the fence clock lets the server owe a window at once,
65
+ * unless the window asks for another number.
66
+ *
67
+ * One is a frame per round trip: the client draws, the server works through
68
+ * it, the reply comes back, and only then does the next frame start. Neither
69
+ * side works while the other does, so a frame costs the sum of the two.
70
+ * Where the reply is slow for reasons of the server's own, most of that sum
71
+ * is waiting. XQuartz answers a fence only once the frame has gone to the
72
+ * macOS window server: 13 ms at the median during a drag, against about
73
+ * 0.1 ms for an idle round trip, and the drag ran at 75-78 fps on a 120 Hz
74
+ * panel (issue #369). With two, the next frame is drawn during that wait,
75
+ * and the same drag runs at 107-118. A slow link gains the same way: a frame
76
+ * per round trip becomes two.
77
+ *
78
+ * Not more, because past two the wait is already overlapped, and each frame
79
+ * queued behind a busy server, or on a link too narrow for it, is a frame of
80
+ * latency. Where a blit goes out through Present it stays one (see
81
+ * _frameLimit).
82
+ */
83
+ const DEFAULT_MAX_FRAMES_IN_FLIGHT = 2;
84
+
85
+ /**
86
+ * `maxFramesInFlight` as given, or a RangeError saying what it takes.
87
+ *
88
+ * Zero is refused, not read as "no limit", which is what `frameInterval: 0`
89
+ * would lead a reader to expect: a window that may have no frame in flight
90
+ * never draws. The way to wait on the server not at all is `frameSync: false`.
91
+ */
92
+ function framesInFlightLimit(n) {
93
+ if (Number.isInteger(n) && n >= 1) return n;
94
+ throw new RangeError(
95
+ `ntk: maxFramesInFlight is a number of frames, 1 or more — got ${String(n)}. ` +
96
+ 'For no limit at all, create the window with frameSync: false.'
97
+ );
98
+ }
99
+
63
100
  /**
64
101
  * The range a measured vblank period has to fall in to be believed, in ms.
65
102
  *
@@ -109,6 +146,32 @@ const REFRESH_QUANTILE = 0.25;
109
146
  const STALL_TIMEOUT = 2000;
110
147
  const STALL_TIMEOUT_FIRST = 250;
111
148
 
149
+ /**
150
+ * The frame period Present makes up for a screen with no vertical blank to
151
+ * offer it, in µs: 60Hz, which xorg-server 21.1 and later compute as
152
+ * `1000000 / 60` and 1.20 and earlier wrote down as 16667.
153
+ *
154
+ * A server started with `-fakescreenfps` at another rate is not recognised.
155
+ * It was asked for that clock, and keeps it.
156
+ */
157
+ const FAKE_MSC_INTERVALS = [16666, 16667];
158
+
159
+ /**
160
+ * How much of the counter a made-up msc has to account for before a window
161
+ * believes it, in vblanks: about a quarter of a second.
162
+ *
163
+ * One completion that fits proves little. A display's counter starts after
164
+ * the server's clock does, so its `ust / msc` falls toward its own period as
165
+ * it runs, and a display a little faster than 60Hz passes through the
166
+ * made-up line on the way down. It stays within half a tick of the line for
167
+ * `interval / (interval - period)` vblanks, so a fit across this span rules
168
+ * out every display more than `interval / span` µs faster than the tick, which
169
+ * is anything past 64Hz. A display closer than that fits only while its
170
+ * counter crosses the line, and only a window whose first frames land in that
171
+ * stretch is fooled.
172
+ */
173
+ const SYNTHETIC_MSC_SPAN = 16;
174
+
112
175
  /**
113
176
  * MotionNotify detail saying "this is the only motion you get until you ask".
114
177
  *
@@ -406,6 +469,9 @@ export default class Window extends Drawable {
406
469
  if (args.id === 0) {
407
470
  throw new Error('ntk: 0 (None) is not a window id');
408
471
  }
472
+ const maxFramesInFlight = framesInFlightLimit(
473
+ args.maxFramesInFlight ?? DEFAULT_MAX_FRAMES_IN_FLIGHT
474
+ );
409
475
  if (args.id) {
410
476
  const cached = Window._cacheFor(app).get(args.id);
411
477
  // TODO: check event mask, if more events in args - update it
@@ -476,7 +542,9 @@ export default class Window extends Drawable {
476
542
  this._frameSyncEnabled = args.frameSync !== false;
477
543
  // which signal ends a frame (see _clockOnPresent): 'auto' takes the
478
544
  // display's own vblank where the Present path can supply it and the fence
479
- // everywhere else, 'fence' pins the round-trip clock
545
+ // everywhere else, including a server whose Present has no display behind
546
+ // it (see _probeMsc); 'present' takes Present's clock even there; 'fence'
547
+ // pins the round-trip clock
480
548
  this._frameClockPref = args.frameClock ?? 'auto';
481
549
  // _NET_WM_SYNC_REQUEST (see enableSyncRequest): the counter the window
482
550
  // manager watches, the value it last asked for, and the watchdog that
@@ -520,6 +588,12 @@ export default class Window extends Drawable {
520
588
  this._refreshSamples = [];
521
589
  this._lastComplete = null;
522
590
  this._clockStalled = false;
591
+ // Whether the msc on those completions is a display's count or a number
592
+ // the server works out from its clock (see _probeMsc): while undecided,
593
+ // the fake intervals every completion so far fits and the msc of the
594
+ // first of them; then the verdict.
595
+ this._mscProbe = { intervals: FAKE_MSC_INTERVALS, since: null };
596
+ this._mscSynthetic = false;
523
597
  // `frameInterval` is a plain number, but whether the caller set it is what
524
598
  // decides its meaning under the vblank clock — an explicit value caps the
525
599
  // frame rate, an unasked-for default must not (see _armTimer). Assigning
@@ -530,12 +604,15 @@ export default class Window extends Drawable {
530
604
  // that probe answers adopts the result when it lands.
531
605
  this._frameInterval = args.frameInterval ?? app.frameInterval ?? DEFAULT_FRAME_INTERVAL;
532
606
  this._frameIntervalExplicit = args.frameInterval !== undefined;
607
+ // how many fences may be outstanding before the next frame waits for one
608
+ // (see _frameLimit)
609
+ this._maxFramesInFlight = maxFramesInFlight;
533
610
  this.frameLatency = null;
534
611
  this._frame = {
535
612
  pending: new Map(), // coalescible event name -> merged event
536
613
  rafCbs: [],
537
614
  rafId: 0,
538
- inFlight: false, // fence round-trip awaiting the server's reply
615
+ fences: 0, // fence round trips awaiting the server's reply
539
616
  presentInFlight: false, // present awaiting the display's CompleteNotify
540
617
  presentAt: 0, // when that present was sent, for frameLatency
541
618
  stallTimer: null,
@@ -1311,10 +1388,16 @@ export default class Window extends Drawable {
1311
1388
  * - a fence, everywhere else: a cheap request with a reply
1312
1389
  * (GetInputFocus) sent after each frame; X processes requests in order,
1313
1390
  * so its reply confirms the server consumed everything the frame drew.
1391
+ * That includes a server whose Present has no display behind it and
1392
+ * makes its vertical blanks up from a timer (see _probeMsc).
1314
1393
  *
1315
- * Either way at most one is outstanding, which is what bounds the work in
1316
- * flight: on a slow connection frames degrade to one per round-trip instead
1317
- * of queueing a trail of stale updates.
1394
+ * Either way the frames outstanding are bounded, which is what bounds the
1395
+ * work in flight: one present on the display's clock, and on the fence
1396
+ * `maxFramesInFlight` fences, two unless the window asked otherwise (see
1397
+ * _frameLimit). On a slow connection frames degrade to that many per round
1398
+ * trip instead of queueing a trail of stale updates. Two rather than one so
1399
+ * that the next frame can be drawn while the server is still working
1400
+ * through the last, instead of each waiting out the other.
1318
1401
  *
1319
1402
  * A timer backs both up. Under the fence it is the rate limit — at most one
1320
1403
  * frame per `frameInterval` ms, so a fast local server isn't asked to redraw
@@ -1335,18 +1418,63 @@ export default class Window extends Drawable {
1335
1418
  */
1336
1419
  _scheduleFrame() {
1337
1420
  const f = this._frame;
1338
- if (f.scheduled || f.inFlight || f.presentInFlight || f.timer) return;
1421
+ if (f.scheduled || this._fencesFull() || f.presentInFlight || f.timer) return;
1339
1422
  f.scheduled = true;
1340
1423
  setImmediate(() => {
1341
1424
  f.scheduled = false;
1342
1425
  // gates may have been armed after this got scheduled (work queued
1343
1426
  // from inside a running frame, e.g. a rAF loop re-registering) —
1344
1427
  // the completion / fence reply / timer expiry will reschedule then
1345
- if (f.inFlight || f.presentInFlight || f.timer) return;
1428
+ if (this._fencesFull() || f.presentInFlight || f.timer) return;
1346
1429
  this._runFrame();
1347
1430
  });
1348
1431
  }
1349
1432
 
1433
+ /**
1434
+ * How many frames may be waiting for their fence before the next one waits
1435
+ * too: `maxFramesInFlight`, except where a blit goes out through Present.
1436
+ *
1437
+ * A second frame is safe behind a CopyArea. The server runs requests in
1438
+ * order, so the copy has read the backing store before anything drawn after
1439
+ * it lands there, and the fence says no more than that the server read the
1440
+ * frame — which is all the next one needs. A present with Option.Copy is
1441
+ * different: it owns the backing store until the copy executes, at the next
1442
+ * vertical blank, and the fence reply says only that the server has read the
1443
+ * present. A frame drawn behind it early can put half of itself on the screen
1444
+ * (the hazard issue #223 describes), and a second frame in flight would
1445
+ * widen that window. So a window whose blits are presents keeps one, whether
1446
+ * the fence or the display is ending its frames — on the display's clock a
1447
+ * present outstanding is the gate anyway, and there is never more than one.
1448
+ *
1449
+ * A window with no backing store has no blit to protect. Its frames go by
1450
+ * the limit as a CopyArea window's do.
1451
+ */
1452
+ _frameLimit() {
1453
+ return this._blitsThroughPresent() ? 1 : this._maxFramesInFlight;
1454
+ }
1455
+
1456
+ /** Have the frames waiting for their fence reached the limit? */
1457
+ _fencesFull() {
1458
+ return this._frame.fences >= this._frameLimit();
1459
+ }
1460
+
1461
+ /**
1462
+ * How many frames the server may owe this window at once, on the fence
1463
+ * clock, before the next one waits for the reply to the oldest: 2 unless
1464
+ * set. `1` is a frame per round trip. Where blits go through Present, or
1465
+ * the display is ending frames, one is all there is, whatever this says —
1466
+ * see _frameLimit.
1467
+ */
1468
+ get maxFramesInFlight() {
1469
+ return this._maxFramesInFlight;
1470
+ }
1471
+
1472
+ set maxFramesInFlight(n) {
1473
+ // A raised limit needs no wakeup: the gate was shut because fences are
1474
+ // outstanding, and the next reply runs what it held back.
1475
+ this._maxFramesInFlight = framesInFlightLimit(n);
1476
+ }
1477
+
1350
1478
  /**
1351
1479
  * Minimum ms between paced frames, and between blits.
1352
1480
  *
@@ -1379,14 +1507,15 @@ export default class Window extends Drawable {
1379
1507
  /**
1380
1508
  * Which clock is ending this window's frames right now: 'present' when the
1381
1509
  * display is, 'fence' when the server round-trip is. Assignable with
1382
- * 'auto' (prefer the display) or 'fence' (never use it).
1510
+ * 'auto' (prefer the display), 'present' (prefer Present's clock even
1511
+ * where no display is behind it) or 'fence' (never use it).
1383
1512
  */
1384
1513
  get frameClock() {
1385
1514
  return this._clockOnPresent() ? 'present' : 'fence';
1386
1515
  }
1387
1516
 
1388
1517
  set frameClock(mode) {
1389
- this._frameClockPref = mode === 'present' ? 'auto' : mode;
1518
+ this._frameClockPref = mode;
1390
1519
  if (this._frameClockPref !== 'fence') this._selectPresentInput();
1391
1520
  }
1392
1521
 
@@ -1400,21 +1529,35 @@ export default class Window extends Drawable {
1400
1529
  * completion that never came drops the window back to the fence until one
1401
1530
  * does, so a clock that stops cannot stop the window with it.
1402
1531
  *
1403
- * `_presentFlipped` is the one-way door — a server that has flipped a copy
1404
- * present is off this path for good, and its blits no longer produce the
1405
- * completions the clock is made of.
1532
+ * `_presentAbandoned` is the one-way door — a window whose server turned
1533
+ * out unable to do what the path is for is off it from then on, and its
1534
+ * blits no longer produce the completions the clock is made of.
1406
1535
  */
1407
1536
  _clockOnPresent() {
1408
1537
  return (
1409
1538
  this._frameClockPref !== 'fence' &&
1410
1539
  this._frameSyncEnabled &&
1411
1540
  !this._clockStalled &&
1412
- !this._presentFlipped &&
1541
+ !this._presentAbandoned() &&
1413
1542
  !!this._presentExt &&
1414
1543
  !!this._presentEid
1415
1544
  );
1416
1545
  }
1417
1546
 
1547
+ /**
1548
+ * Has this window stopped presenting through the extension, the blit along
1549
+ * with the clock, because of what its server turned out to be?
1550
+ *
1551
+ * Two things close the door. A server that flipped a present sent with
1552
+ * Option.Copy owns the pixmap it was handed (see _handlePresentEvent). A
1553
+ * server whose msc is made up has no display behind the extension, and
1554
+ * only holds each copy back for a timer (see _probeMsc) — that one stays
1555
+ * open to a caller who asked for Present's clock by name.
1556
+ */
1557
+ _presentAbandoned() {
1558
+ return !!this._presentFlipped || (this._mscSynthetic && this._frameClockPref !== 'present');
1559
+ }
1560
+
1418
1561
  _runFrame() {
1419
1562
  const f = this._frame;
1420
1563
  if (
@@ -1585,15 +1728,24 @@ export default class Window extends Drawable {
1585
1728
  for (const [name, ev] of pending) this.emit(name, ev);
1586
1729
  }
1587
1730
 
1731
+ /**
1732
+ * Fence the frame that just went out: one request with a reply, answered
1733
+ * once the server has read everything before it.
1734
+ *
1735
+ * Every frame gets its own, so `fences` is exactly the number of frames the
1736
+ * server has not confirmed. Nothing here refuses one past the limit — the
1737
+ * callers only send a frame while the gate is open, and a frame sent some
1738
+ * other way is still better counted than left unfenced, where the gate
1739
+ * would open with it unconfirmed.
1740
+ */
1588
1741
  _armFence() {
1589
1742
  if (!this._frameSyncEnabled) return;
1590
1743
  const f = this._frame;
1591
- if (f.inFlight) return;
1592
1744
  const start = performance.now();
1593
1745
  safeRelease(this.X, () => {
1594
- f.inFlight = true;
1746
+ f.fences++;
1595
1747
  this.X.GetInputFocus(() => {
1596
- f.inFlight = false;
1748
+ f.fences--;
1597
1749
  this.frameLatency = performance.now() - start;
1598
1750
  this._frameEnded();
1599
1751
  });
@@ -1823,6 +1975,7 @@ export default class Window extends Drawable {
1823
1975
  f.presentInFlight = false;
1824
1976
  this.frameLatency = now - f.presentAt;
1825
1977
  this._sampleRefresh(ev, now);
1978
+ this._probeMsc(ev);
1826
1979
  this._frameEnded();
1827
1980
  }
1828
1981
 
@@ -1890,6 +2043,60 @@ export default class Window extends Drawable {
1890
2043
  this.refreshInterval = sorted[Math.floor(REFRESH_QUANTILE * (sorted.length - 1))];
1891
2044
  }
1892
2045
 
2046
+ /**
2047
+ * Is a display counting these frames, or is the server making the count up?
2048
+ *
2049
+ * Present waits for vertical blanks, and a screen whose driver has none to
2050
+ * offer gets fake ones: xserver's present_fake.c computes the msc from its
2051
+ * clock, `(ust + interval / 2) / interval`, and completes a present when a
2052
+ * timer set for the next tick fires. That is XQuartz, Xvfb, Xephyr, Xnest,
2053
+ * the VNC servers and Xorg on a driver without Present support. The
2054
+ * vblank clock buys nothing there and costs throughput. It paces to a 60Hz
2055
+ * timer rather than to the panel, which on XQuartz is the macOS
2056
+ * compositor's business. And a present arriving past the middle of a
2057
+ * period already counts as the next tick, and is aimed at the one after
2058
+ * that, so a frame that takes more than half a period to turn round waits
2059
+ * two (issue #366).
2060
+ *
2061
+ * The blit goes with the clock, as it does for a server that flips: the
2062
+ * copy waits for the same timer, so a window on the fence would draw
2063
+ * faster yet still update at most 60 times a second, and a copy could
2064
+ * read a frame half drawn, because the fence reply only says the server
2065
+ * has read the present, not run it. CopyArea runs in request order.
2066
+ *
2067
+ * So the question is whether `msc` is `ust` rounded to the nearest tick.
2068
+ * A display's counter cannot keep to that for long (SYNTHETIC_MSC_SPAN),
2069
+ * and a single completion off the line settles it the other way. Either
2070
+ * verdict is final: whether Present has a driver behind it is settled per
2071
+ * screen, when the server starts.
2072
+ */
2073
+ _probeMsc(ev) {
2074
+ const probe = this._mscProbe;
2075
+ if (!probe) return;
2076
+ const intervals = probe.intervals.filter((interval) => {
2077
+ // msc === floor((ust + half) / interval), in integers
2078
+ const half = Math.floor(interval / 2);
2079
+ const offset = ev.ust - ev.msc * interval;
2080
+ return offset >= -half && offset < interval - half;
2081
+ });
2082
+ if (!intervals.length) {
2083
+ this._mscProbe = null; // a count, so a display
2084
+ return;
2085
+ }
2086
+ probe.intervals = intervals;
2087
+ probe.since ??= ev.msc;
2088
+ if (ev.msc - probe.since < SYNTHETIC_MSC_SPAN) return;
2089
+ this._mscProbe = null;
2090
+ this._mscSynthetic = true;
2091
+ // What was measured was a timer: no display period to report, and no
2092
+ // display to have dropped frames on. A window kept on this clock by
2093
+ // `frameClock: 'present'` learns the timer's period again from here.
2094
+ this.refreshInterval = null;
2095
+ this._refreshSamples = [];
2096
+ this._lastComplete = null;
2097
+ this.droppedFrames = 0;
2098
+ }
2099
+
1893
2100
  /**
1894
2101
  * Guarantee that a frame ends even if its completion does not arrive.
1895
2102
  *
@@ -2033,8 +2240,9 @@ export default class Window extends Drawable {
2033
2240
  * DOM-style requestAnimationFrame: `cb(now)` runs on this window's next
2034
2241
  * paced frame — one per vertical blank where the display is the clock (see
2035
2242
  * _clockOnPresent), otherwise at most once per `frameInterval` ms — and
2036
- * always with at most one frame outstanding, so animation loops adapt to
2037
- * the output's rate and to connection latency instead of flooding either.
2243
+ * always with a bounded number of frames outstanding (one on the display's
2244
+ * clock, `maxFramesInFlight` on the fence), so animation loops adapt to the
2245
+ * output's rate and to connection latency instead of flooding either.
2038
2246
  * Returns an id for cancelAnimationFrame().
2039
2247
  */
2040
2248
  requestAnimationFrame(cb) {
@@ -2053,9 +2261,12 @@ export default class Window extends Drawable {
2053
2261
 
2054
2262
  /**
2055
2263
  * Whether a blit this window already owes is still waiting to go out —
2056
- * because the last frame is unanswered (its fence unreplied, or its present
2057
- * not yet on the display), or because a present is deferred behind the
2058
- * minimum inter-blit interval.
2264
+ * because the frames already sent are unanswered (as many fences unreplied
2265
+ * as `maxFramesInFlight` allows, or a present not yet on the display), or
2266
+ * because a present is deferred behind the minimum inter-blit interval.
2267
+ *
2268
+ * So it reads false with a frame in flight when the limit allows another:
2269
+ * a blit drawn then goes out at once, which is the question this answers.
2059
2270
  *
2060
2271
  * The one bit of frame-clock state worth publishing, because it is the
2061
2272
  * difference between the two ways a toolkit can answer a discrete input.
@@ -2082,16 +2293,17 @@ export default class Window extends Drawable {
2082
2293
  * asked for both gets the unpaced behaviour those options ask for.
2083
2294
  */
2084
2295
  frameInFlight() {
2085
- return this._frame.inFlight || this._frame.presentInFlight || this._presentPending;
2296
+ return this._fencesFull() || this._frame.presentInFlight || this._presentPending;
2086
2297
  }
2087
2298
 
2088
2299
  _present() {
2089
2300
  if (!this._backing) return;
2090
2301
  const f = this._frame;
2091
2302
  const wait = this._presentWait();
2092
- if (f.inFlight || f.presentInFlight || wait > 0) {
2093
- // the server hasn't confirmed the previous frame yet, the display hasn't
2094
- // shown it, or the last blit was too recent — defer, leaving a wakeup
2303
+ if (this._fencesFull() || f.presentInFlight || wait > 0) {
2304
+ // the server hasn't confirmed enough of the frames already sent, the
2305
+ // display hasn't shown the last one, or the last blit was too recent —
2306
+ // defer, leaving a wakeup
2095
2307
  this._deferPresent(wait);
2096
2308
  return;
2097
2309
  }
@@ -2146,7 +2358,7 @@ export default class Window extends Drawable {
2146
2358
  if (!this._presentPending && !this._dirty) return; // someone blitted it
2147
2359
  const again = this._presentWait();
2148
2360
  if (again > 0) return this._deferPresent(again); // a paced frame re-stamped
2149
- if (f.inFlight || f.presentInFlight) return; // the frame's end is closer
2361
+ if (this._fencesFull() || f.presentInFlight) return; // the frame's end is closer
2150
2362
  this._presentNow();
2151
2363
  if (!f.presentInFlight) this._armFence();
2152
2364
  }, wait);
@@ -2227,15 +2439,25 @@ export default class Window extends Drawable {
2227
2439
  });
2228
2440
  }
2229
2441
 
2442
+ /**
2443
+ * Would a blit go out through the Present extension right now, rather than
2444
+ * as CopyArea? Not before the extensions have answered, and not once the
2445
+ * window has left the path (see _presentAbandoned).
2446
+ */
2447
+ _blitsThroughPresent() {
2448
+ return (
2449
+ !!this._presentExt && !!this._fixesExt && !!this._updateRegion && !this._presentAbandoned()
2450
+ );
2451
+ }
2452
+
2230
2453
  /**
2231
2454
  * The Present form of the blit. Returns false when it did not happen, so
2232
2455
  * the caller falls back to CopyArea — which is also what runs before the
2233
2456
  * extensions have answered.
2234
2457
  */
2235
2458
  _presentWithExtension(rects) {
2459
+ if (!this._blitsThroughPresent()) return false;
2236
2460
  const P = this._presentExt;
2237
- if (!P || !this._fixesExt || !this._updateRegion) return false;
2238
- if (this._presentFlipped) return false; // see _handlePresentEvent
2239
2461
  safeRelease(this.X, () => {
2240
2462
  this._fixesExt.SetRegion(
2241
2463
  this._updateRegion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.11.0",
3
+ "version": "8.12.0",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",