/* * This is a TypeScript port of the original Java version, which was written by * Gil Tene as described in * https://github.com/HdrHistogram/HdrHistogram * and released to the public domain, as explained at * http://creativecommons.org/publicdomain/zero/1.0/ */ import { NO_TAG } from "./Histogram"; import { decodeFromCompressedBase64 } from "./encoding"; import Histogram, { BitBucketSize } from "./Histogram"; const TAG_PREFIX = "Tag="; const TAG_PREFIX_LENGTH = "Tag=".length; /** * A histogram log reader. *

* Histogram logs are used to capture full fidelity, per-time-interval * histograms of a recorded value. *

* For example, a histogram log can be used to capture high fidelity * reaction-time logs for some measured system or subsystem component. * Such a log would capture a full reaction time histogram for each * logged interval, and could be used to later reconstruct a full * HdrHistogram of the measured reaction time behavior for any arbitrary * time range within the log, by adding [only] the relevant interval * histograms. *

Histogram log format:

* A histogram log file consists of text lines. Lines beginning with * the "#" character are optional and treated as comments. Lines * containing the legend (starting with "Timestamp") are also optional * and ignored in parsing the histogram log. All other lines must * be valid interval description lines. Text fields are delimited by * commas, spaces. *

* A valid interval description line contains an optional Tag=tagString * text field, followed by an interval description. *

* A valid interval description must contain exactly four text fields: *

* The log file may contain an optional indication of a starting time. Starting time * is indicated using a special comments starting with "#[StartTime: " and followed * by a number parse-able as a double, representing the start time (in seconds) * that may be added to timestamps in the file to determine an absolute * timestamp (e.g. since the epoch) for each interval. */ class HistogramLogReader { startTimeSec: number; baseTimeSec: number; lines: string[]; currentLineIndex: number; bitBucketSize: BitBucketSize; useWebAssembly: boolean; constructor( logContent: string, bitBucketSize: BitBucketSize = 32, useWebAssembly: boolean = false ) { this.lines = splitLines(logContent); this.currentLineIndex = 0; this.bitBucketSize = bitBucketSize; this.useWebAssembly = useWebAssembly; } /** * Read the next interval histogram from the log. Returns a Histogram object if * an interval line was found, or null if not. *

Upon encountering any unexpected format errors in reading the next interval * from the file, this method will return a null. * @return a DecodedInterval, or a null if no appropriate interval found */ public nextIntervalHistogram( rangeStartTimeSec = 0, rangeEndTimeSec = Number.MAX_VALUE ): Histogram | null { while (this.currentLineIndex < this.lines.length) { const currentLine = this.lines[this.currentLineIndex]; this.currentLineIndex++; if (currentLine.startsWith("#[StartTime:")) { this.parseStartTimeFromLine(currentLine); } else if (currentLine.startsWith("#[BaseTime:")) { this.parseBaseTimeFromLine(currentLine); } else if ( currentLine.startsWith("#") || currentLine.startsWith('"StartTimestamp"') ) { // skip legend & meta data for now } else if (currentLine.includes(",")) { const tokens = currentLine.split(","); const [firstToken] = tokens; let tag: string; if (firstToken.startsWith(TAG_PREFIX)) { tag = firstToken.substring(TAG_PREFIX_LENGTH); tokens.shift(); } else { tag = NO_TAG; } const [ rawLogTimeStampInSec, rawIntervalLengthSec, , base64Histogram, ] = tokens; const logTimeStampInSec = Number.parseFloat(rawLogTimeStampInSec); if (!this.baseTimeSec) { // No explicit base time noted. Deduce from 1st observed time (compared to start time): if (logTimeStampInSec < this.startTimeSec - 365 * 24 * 3600.0) { // Criteria Note: if log timestamp is more than a year in the past (compared to // StartTime), we assume that timestamps in the log are not absolute this.baseTimeSec = this.startTimeSec; } else { // Timestamps are absolute this.baseTimeSec = 0.0; } } if (rangeEndTimeSec < logTimeStampInSec) { return null; } if (logTimeStampInSec < rangeStartTimeSec) { continue; } const histogram = decodeFromCompressedBase64( base64Histogram, this.bitBucketSize, this.useWebAssembly ); histogram.startTimeStampMsec = (this.baseTimeSec + logTimeStampInSec) * 1000; const intervalLengthSec = Number.parseFloat(rawIntervalLengthSec); histogram.endTimeStampMsec = (this.baseTimeSec + logTimeStampInSec + intervalLengthSec) * 1000; histogram.tag = tag; return histogram; } } return null; } private parseStartTimeFromLine(line: string) { this.startTimeSec = Number.parseFloat(line.split(" ")[1]); } private parseBaseTimeFromLine(line: string) { this.baseTimeSec = Number.parseFloat(line.split(" ")[1]); } } const splitLines = (logContent: string) => logContent.split(/\r\n|\r|\n/g); const shouldIncludeNoTag = (lines: string[]) => lines.find( (line) => !line.startsWith("#") && !line.startsWith('"') && !line.startsWith(TAG_PREFIX) && line.includes(",") ); export const listTags = (content: string) => { const lines = splitLines(content); const tags = lines .filter((line) => line.includes(",") && line.startsWith(TAG_PREFIX)) .map((line) => line.substring(TAG_PREFIX_LENGTH, line.indexOf(","))); const tagsWithoutDuplicates = new Set(tags); const result = Array.from(tagsWithoutDuplicates); if (shouldIncludeNoTag(lines)) { result.unshift("NO TAG"); } return result; }; export default HistogramLogReader;