node-srt/src/async-write-modes.js
Stephan Hesse bf60795889
Async API server/streams impl & functional tests + Allow to use local SRT source repo instead of remote (#9)
* replace stream module by improved version of readable/writable impl

* rm server.js

* async api improvments:
- better tracing of calls from worker back and forth
- fix transferrable handling to avoid copying buffers for r/w
- optional debug logs
- completed jsdocs annotations
- add dispose method
- add setLogLevel method (analoguous to added binding)

* node-srt C bindings:
- add SetLogLevel to get libSRT log output if desired
- add OK static member
- add #define EPOLL_EVENTS_NUM_MAX 1024
- improve error string thrown in Read (add that it comes from srt_recvmsg)
- improve error string thrown in Write (add that it comes from srt_sendmsg2)
- misc isofunctional improvements (var names) and comments

* add SRT logging related JS-side helper

* rewrite flat TypeScript decl files without "module" keyword

* add ts enum decl for all libSRT enums

* async-worker: enable using transferrable for zero-copy
+ allow better debugging (like in api/dispatcher side)
+ misc improvements on code quality

* add async-helpers: various functions to help dealing with transferrables
+ tracing calls to native bindings in debug output

* add async read/write modes functions + async-reader-writer class
- these will allow for performing high-level r/w operations conveniently
at optimum throughput for larger pieces of payload i.e list of packets.

* add srt-server and srt-connection (can manage multiple clients),
- based on async-api
- can be used with reader/writer (i.e the underlying modes)

* srt-server/connection typings

* async srt spec: add dispose method usage (but commented out as crashing atm)

* async srt spec: rm redundant checks on SRT static members (they are done
in other spec already)

* promises api spec: formal fixes

* stream spec: add dummy test

* package.json:
- put gyp toolchain in runtime deps (since the build happens on install)
- add JEST test runner
- shorten check-tsc script
- rebuild script: check & use all CPU cores available
- run rebuild actually on install, not preinstall (fixes deps not being there)
- remove preinstall and thus "npm install git-clone" in the package scripts

* update package lock

* update typings index not to need triple-slashs anymore

* in srt.ts example: check for read return value type

* build-srt-sdk script:
- allow to use any local libSRT code repo
- when using make: use all amount of cores available for build
- isolate better code running on different platforms

* update package main index with new things

* add enum typings index

* add jest config

* add "use strict" on async-srt-await example

* add integration/smoke testing for client-to-server one-way burst write

* readme: add note on build prerequisites

* readme: add infos on new components SRTServer/Connection & AsyncReaderWriter
2020-10-21 13:09:52 +02:00

204 lines
7.7 KiB
JavaScript

const { SRT } = require('../build/Release/node_srt.node');
/**
* @module async-write-modes
*
* @author Stephan Hesse <stephan@emliri.com>
* @copyright EMLIRI, Stephan Hesse (c) 2020
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*
*/
/**
*
* @function
*
* Description:
*
* This function allows to dispatch in high-performing mode a number
* of sequential `write()` calls to an `AsyncSRT` instance for a given `socketFd`.
*
* Specifications:
*
* - It allows for burst-writes to achieve throughput peaks as necessary in latency-critical
* applications.
*
* - The function is passed an array of data-buffer slices
* (that need to be individually "neuterable" or referencing a shared memory,
* see `SharedArrayBuffer` in JavaScript language documentations),
* which may be referred to packets.
*
* - The chunks size can not exceed the system or network
* specific MTU which the underlying SRT write binding will be able to accept.
*
* - It operates fully non-blocking. The main-thread is only used for queuing write-calls,
* and the maximum occupation time per iteration for queuing calls is parameterizable
* (see `writesPerTick`). If you figure that main-thread slots are too long when calling this
* function, consider adapting that parameter (which will cause scheduling overhead however
* to how this function runs).
*
* - When using `ArrayBuffer`, i.e not using `SharedArrayBuffer` as a memory implementation:
* It expects an array of buffers (chunks), which are referencing independent non
* overlapping memory buffers, which is critical here as there will be no copy performed,
* but each underlying buffer that is passed to the write-method will get neutered
* by the worker-thread it is passed to.
*
* Performance:
*
* This note below is here mainly to explain how
* this mode could potentially result in a different runtime-internal execution
* (and performance) then another implementation, the "explict scheduling" one.
*
* What we call a "Yielding Loop" will attempt to use any task-slot available by the runtime
* for queing writes (i.e transferring payload ownership to a worker),
* while ensuring a limited number of calls per main-loop (that is corresponding to how the tick()
* function here is invoked).
*
* By it's usage of the async/await paradigm, it causes the runtime
* to internally use generators as promise-executor and implicitely involves a `yield`.
*
* "yielding" means, stepping in and out of the async function context
* at a given point while maintaining context, while stepping back
* in "asap" when the awaited promise gets resolved - here inside the while loop.
* In order to allow for that context preservation and stack resumage (via async/await),
* this will result in so-called generator-functions internally
* with the JavaScript runtime. Now, depending how all that is implemented,
* there can be an overhead to doing that, in comparison with different,
* more hands-on "explicit" approaches to scheduling write calls,
* that are not using generators.
*
* @param {AsyncSRT} asyncSrt
* @param {number} socketFd
* @param {Array<Uint8Array>} chunks
* @param {Function} onWrite
* @param {number} writesPerTick
* @returns {Promise<void>}
*
*/
async function writeChunksWithYieldingLoop(asyncSrt, socketFd, chunks,
onWrite = null, writesPerTick = 1) {
let chunkIndex = 0;
let chunkWrittenIdx = 0;
while(chunkIndex < chunks.length) await tick();
function tick() {
const writeResultPromises = [];
for (let i = 0; i < writesPerTick; i++) {
if(chunkIndex >= chunks.length) {
break;
}
const chunkBuf = chunks[chunkIndex++];
const whenWritten = asyncSrt.write(socketFd, chunkBuf);
writeResultPromises.push(whenWritten);
whenWritten.then((writeRes) => {
if (writeRes === SRT.ERROR) {
throw new Error('AsyncSRT.write() failed');
}
if (onWrite) {
onWrite(writeRes, chunkWrittenIdx);
}
chunkWrittenIdx++
});
}
return Promise.all(writeResultPromises);
}
}
/**
*
* This is (almost) isofunctional to the yielding-loop (when intervalMs = 0),
* but implemented without using the runtimes generator-function support. Instead,
* we explicitely schedule all write calls using plain-old main-loop timers.
* When we do that setting "immediate tasks" by using a zero-timeout value,
* it will result in the exact same behavior from the perspective of the runtime,
* to use any available slot potentially (as the "yielding loop" mode).
*
* However the yielding-loop "awaits" until all writes that had been
* dispatched are resolved, while here we just keep pressuring the event queue.
*
* TODO: Implement rescheduling based on write-resolution (optional).
*
* The clear advantage of this is the explict nature of scheduling,
* allow pace of calls to be throttled by the set interval value.
* This can be very useful when we can afford to write data not asap,
* but in a given minimal rate instead of bursts and avoid peaking CPU with that.
*
* When using zero as interval timeout, in principle this should perform
* almost exactly like the generator-based mode (using any available
* slot for running a tick()).
*
* But the explicit scheduling has in theory also the advantage to have
* less runtime overhead (as it does not use any generator-function based features).
* However, this may depend on how tasks get prioritized in the end also,
* or how well the runtime is optimized and implemented for one or the other.
* Maybe an await-resolution gets more attention to appear on the main-loop
* than the scheduled interval, if it has immediate timeout (i.e 0), or the overhead
* caused by yielding generators is neglectable.
*
* @param {AsyncSRT} asyncSrt
* @param {number} socketFd
* @param {Array<Uint8Array>} chunks
* @param {Function} onWrite
* @param {number} writesPerTick
* @param {number} intervalMs
*/
function writeChunksWithExplicitScheduling(asyncSrt, socketFd, chunks,
onWrite = null, writesPerTick = 1, intervalMs = 0) {
let chunkIndex = 0;
let chunkWrittenIdx = 0;
// schedule tick-interval
const writeTimer = setInterval(tick, intervalMs);
// run once directly in current stack
tick();
function tick() {
for (let i = 0; i < writesPerTick; i++) {
if(chunkIndex >= chunks.length) {
clearInterval(writeTimer);
break;
}
const chunkBuf = chunks[chunkIndex++];
asyncSrt.write(socketFd, chunkBuf)
.then((writeRes) => {
if (writeRes === SRT.ERROR) {
throw new Error('AsyncSRT.write() failed');
}
if (onWrite) {
onWrite(writeRes, chunkWrittenIdx);
}
chunkWrittenIdx++
});
}
}
}
module.exports = {
writeChunksWithYieldingLoop,
writeChunksWithExplicitScheduling
}