vrack-db
Version:
This is an In Memory database designed for storing time series (graphs).
397 lines (350 loc) • 13.3 kB
text/typescript
/*
* Copyright © 2023 Boris Bobylev. All rights reserved.
* Licensed under the Apache License, Version 2.0
*/
import Interval from "./Interval";
import IStorage from "./LayerStorage/IStorage";
import MetricResult from "./MetricResult";
import MetricWrite from "./MetricWrite";
import LayerStorage, { StorageTypes } from "./LayerStorage/LayerStorage";
import ErrorManager from "./Errors/ErrorManager";
import Typing from "./Typing";
/**
* Metric storage object interface
* It is allowed to use null for a value when
* no value can be obtained for a given time
*/
export interface IMetric {
/** Time in MTU */
time: number,
value: number | null
}
/***
* Intefrace to output the results of the metrics query
**/
export interface IMetricReadResult {
/** result relevance flag */
relevant: boolean
/** Beginning of the period */
start: number,
/** End of period */
end: number,
/** Metrics array */
rows: Array<IMetric>
}
export interface ILayerOptions {
/**
* Defines the accuracy with which the data will be stored,
* for example 10 - 10 MTU. After specifying 10 MTU, this layer will not be able to store data for more than 10 MTU.
* */
interval: number,
/**
* Determines the total data storage period,
* 100 - 100 MTU , i.e. this layer will not be able to store data for more than 100 MTU.
*/
period: number,
/**
* Value storage type
* @see StorageTypes
*/
vStorage?: StorageTypes | null,
/**
* Time storage type
* @see StorageTypes
*/
tStorage?: StorageTypes | null,
/**
* Class interval - Determines the MTU for a new layer.
*/
CInterval?: typeof Interval
}
ErrorManager.register('Aht5U892ICXv', 'VDB_LAYER_SIZE', 'Incorrect size of layer (interval > period)')
ErrorManager.register('iQT3d2SflYQY', 'VDB_LAYER_SECTION', 'Incorrect section - The beginning is greater than the end')
ErrorManager.register('ir1hjNJ0GA0j', 'VDB_LAYER_INITIAL_DATA', 'Incorrect interval or period type, values must be integer and greater than zero')
ErrorManager.register('VWkc3gQ9g1DM', 'VDB_LAYER_VALUE', 'Incorrect type given, type number is required')
ErrorManager.register('gVCGXR03XycW', 'VDB_LAYER_TIME', 'Incorrect type given, type integer is required')
ErrorManager.register('5I1vWpnAH2zM', 'VDB_LAYER_PRECISION', 'Incorrect type given, values must be integer and greater than zero')
/**
* A layer is a low level for storing information.
* The layer works with such a concept as MTU - minimum time unit. MTU is a unit of time represented as an integer.
* For class Interval MTU = 1 second. For IntervalMs MTU = 1 millisecond. For class IntervalUs = microsecond
*
* Example of creating a layer with 10 memory cells
*
* ```ts
* const lay = new Layer({ interval: 1, period: 10})
* ```
*
* For more details, it is recommended to read the official guide
*
* @link https://github.com/ponikrf/VRackDB/wiki/How-It-Works
*
*/
export default class Layer {
/** Layer accuracy in MTU */
protected _interval: number;
/** Period of layer in MTU */
protected _period: number;
/** Number of intervals in a layer */
protected _cells: number
/** Initial point of time in the layer */
protected _startTime: number
/** End point of time in the layer */
protected _endTime: number
/** Value store */
protected _valueStorage: IStorage
/** Time Vault */
protected _timeStorage: IStorage
/** Interval class (see Interval, IntervalMs, IntervalUs) */
CInterval: typeof Interval = Interval
/**
* Creating a new storage with a certain accuracy (interval) and size (period)
*
* **The interval cannot be less than the period**
*
* @example new Layer({ interval: 1, period: 10}) // Creates a layer with an interval of 1 MTU and a period of 10 MTU
*
* @see ILayerOptions
* @link https://github.com/ponikrf/VRackDB/wiki/How-It-Works
*/
constructor({ interval, period, vStorage = null, tStorage = null, CInterval = Interval }: ILayerOptions) {
if (interval >= period) throw ErrorManager.make(new Error, 'VDB_LAYER_SIZE', { interval, period })
if (!Typing.isUInt(interval) || !Typing.isUInt(period)) throw ErrorManager.make(new Error, 'VDB_LAYER_INITIAL_DATA', { interval, period })
this._interval = interval
this._period = period
this.CInterval = CInterval
// get cells count in layer
this._cells = Math.floor(period / this._interval)
this._timeStorage = LayerStorage.make(tStorage, StorageTypes.Uint64, this._cells)
this._valueStorage = LayerStorage.make(vStorage, StorageTypes.Float, this._cells)
const time = this.CInterval.now()
// Fill basic data for new layer
this._startTime = this.CInterval.roundTime(time - (this._interval * (this._cells - 1)), this._interval)
this._endTime = this.CInterval.roundTime(time, this._interval)
}
/**
* Clear layer data
*/
clear() {
this._timeStorage.buffer.fill(0)
}
/**
* Returns the size of the layer in bytes
*/
get length (){
return this._valueStorage.buffer.length + this._timeStorage.buffer.length
}
/**
* Layer accuracy in MTU
*/
get interval() {
return this._interval
}
/**
* Number of intervals in a layer
*
* Old method timeSize is deprecated & deleted
*/
get period() {
return this._period
}
/**
* Number of intervals in a layer
*/
get cells() {
return this._cells
}
/**
* Initial point of time in the layer
*/
get startTime() {
return this._startTime
}
/**
* End point of time in the layer
* */
get endTime() {
return this._endTime
}
/**
* Writes the value into the layer
*
* Automatically adjusts the layer time (startTime/endTime).
*
* **It is necessary to take into account that the layer may not work correctly
* If you write data from different time intervals larger than the layer size**.
*
* Works best when data is written sequentially (in time)
*
* @param {number} time Time in MTU
* @param {number} value Value to be recorded
* @param {string} func Modification function
*/
write(time: number, value: number, func = 'last') {
if (typeof value !== 'number') throw ErrorManager.make(new Error, 'VDB_LAYER_VALUE', { value })
if (typeof time !== 'number') throw ErrorManager.make(new Error, 'VDB_LAYER_TIME', { time })
const oTime = this.CInterval.roundTime(time, this._interval)
/**
* If we receive a value with a time that exceeds the length of our buffer
* We consider that there is no more actual data in this buffer and we set
* startTime equal to our endTime and get a buffer with 1 value
*/
if (this._endTime < (oTime - this._cells * this._interval)) this._startTime = oTime
/**
* We've got a value less than the start of the layer
* We need to rebuild it, so we need this.endTime
* To be less than oTime
* Then the following condition will do it for us
*/
if (oTime < this._startTime) {
this._endTime = oTime - this._interval
}
/**
* We've got a time value greater than the last time, so we need to move
* the end of our queue to the next index and replace endTime
*/
if (this._endTime < oTime) {
const sTime = this.CInterval.roundTime(time - (this._interval * (this._cells - 1)), this._interval)
this._startTime = sTime
this._endTime = oTime
}
const index = this.getIndex(time)
this.writeBuffer(index, oTime, value, func)
}
/**
* Returns data in the interval start -> end with layer accuracy
*
* If the specified start and end are outside the layer, they will be converted to layer frames
* this will be indicated in the result
*
* Start -> end parameters are automatically rounded to layer accuracy
*
* If there is a need to get data with arbitrary precision
* it is better to use `readCustomInterval`.
*
* @see readCustomInterval
*
* @param {number} start Start time
* @param {number} end End time
*/
readInterval(start: number, end: number) {
if (start < this._startTime) start = this._startTime
if (end > this._endTime) end = this._endTime
if (start > end) end = this._endTime
const ils = this.CInterval.getIntervals(start, end, this._interval)
const result: IMetricReadResult = { relevant: true, start, end, rows: [] }
for (let i = 0; i < ils.length; i++) result.rows.push(this.readOne(ils[i]))
return result
}
/**
* Allows data to be read from a layer at a different precision than the layer's native one
* Supports both smaller intra-layer precision and larger intra-layer precision.
* This can be useful for looking at a complex graph in more detail,
* or aggregating data with the LAST function to view a less detailed graph.
*
* If the specified start and end are outside the layer, they will be brought to the layer boundaries.
* this will be indicated in the result
*
* @param {number} start Start time
* @param {number} end End time
* @param {number} precision
*/
readCustomInterval(start: number, end: number, precision: number, func = 'last'): IMetricReadResult {
if (!Typing.isUInt(precision)) throw ErrorManager.make(new Error, 'VDB_LAYER_PRECISION', { precision })
if (start < this._startTime) start = this._startTime
if (end > this._endTime) end = this._endTime
if (!precision) precision = this._interval
if (start > end) throw ErrorManager.make(new Error, 'VDB_LAYER_SECTION', { start, end })
const result: IMetricReadResult = { relevant: true, start, end, rows: [] }
const ils: Array<number> = this.CInterval.getIntervals(start, end, precision)
let to: number;
for (let i = 0; i < ils.length; i++) {
if (this._startTime > ils[i]) continue
if (precision <= this._interval) { to = ils[i]; }
else { to = (ils[i] + precision) - this._interval; if (to < ils[i]) to = ils[i] }
const tres = this.readInterval(ils[i], to) // Count the interval with data
const val: IMetric = {
time: ils[i],
value: MetricResult.aggregate(tres, func)
}
result.rows.push(val)
}
return result
}
/**
* Return all points in layer
* See example!
*
* @example console.table(layer.dump())
*/
dump() {
const rows: Array<IMetric> = []
for (let i = 0; i < this._cells; i++) rows.push(this.readBuffer(i))
return rows
}
/**
* Reads 1 index by time and retrieves its value
*
* @param {number} time time
*/
protected readOne(time: number): IMetric {
const metric = this.readBuffer(this.getIndex(time))
metric.time = time
return metric
}
/**
* Returns the time index
*
* @param {number} time time in MTU
*/
protected getIndex(time: number) {
return Math.floor(this._cells + time / this._interval) % this._cells;
}
/**
* Checks if the time is valid for the current state
* of the layer, in fact it checks if the time is within the interval
* of the layer's current active time
*
* @param {number} time Time in MTU
*/
protected validTime(time: number) {
return (time >= this._startTime && time <= this._endTime)
}
/**
* Reading metrics by index
*
* @param {number} index Metrics Index
* @returns {IMetric} Metric value
*/
protected readBuffer(index: number): IMetric {
const time = this._timeStorage.readBuffer(index)
let value: number | null = this._valueStorage.readBuffer(index)
if (!this.validTime(time)) value = null
return { time, value }
}
/**
* Writes data to the buffer
*
* @param {number} index Metric Index
* @param {number} time Time for recording
* @param {number} value Value for recording
* @param {string} func Modification function
*/
protected writeBuffer(index: number, time: number, value: number, func = 'last') {
value = this.modifyWrite(index, value, func)
this._timeStorage.writeBuffer(index, time)
this._valueStorage.writeBuffer(index, value)
}
/**
* Modifies the data before writing the buffer
*
* @param {number} index Metrics Index
* @param {number} value Value for recording
* @param {string} func Modification function
*/
protected modifyWrite(index: number, value: number, func: string) {
const val = this.readBuffer(index)
if (val.value === null) return value
return MetricWrite.modify(val.value, value, func)
}
}