diff --git a/BatteryMeter.c b/BatteryMeter.c index 001249d4c..7ab1bac97 100644 --- a/BatteryMeter.c +++ b/BatteryMeter.c @@ -64,6 +64,7 @@ const MeterClass BatteryMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 1, + .isPercentChart = true, .total = 100.0, .attributes = BatteryMeter_attributes, .name = "Battery", diff --git a/CPUMeter.c b/CPUMeter.c index 1451ed77a..9345b96b2 100644 --- a/CPUMeter.c +++ b/CPUMeter.c @@ -348,6 +348,7 @@ const MeterClass CPUMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = CPU_METER_ITEMCOUNT, + .isPercentChart = true, .total = 100.0, .attributes = CPUMeter_attributes, .name = "CPU", diff --git a/DiskIOMeter.c b/DiskIOMeter.c index 8af7d9563..f0698692d 100644 --- a/DiskIOMeter.c +++ b/DiskIOMeter.c @@ -154,6 +154,7 @@ const MeterClass DiskIOMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 1, + .isPercentChart = true, .total = 100.0, .attributes = DiskIOMeter_attributes, .name = "DiskIO", diff --git a/FileDescriptorMeter.c b/FileDescriptorMeter.c index cd3baf58c..bd7585cfd 100644 --- a/FileDescriptorMeter.c +++ b/FileDescriptorMeter.c @@ -110,6 +110,7 @@ const MeterClass FileDescriptorMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 2, + .isPercentChart = false, .total = 65536.0, .attributes = FileDescriptorMeter_attributes, .name = "FileDescriptors", diff --git a/LoadAverageMeter.c b/LoadAverageMeter.c index 6bf13a03e..a0b05f2be 100644 --- a/LoadAverageMeter.c +++ b/LoadAverageMeter.c @@ -46,15 +46,15 @@ static void LoadAverageMeter_updateValues(Meter* this) { this->curItems = 1; // change bar color and total based on value + if (this->total < this->host->activeCPUs) { + this->total = this->host->activeCPUs; + } if (this->values[0] < 1.0) { this->curAttributes = OK_attributes; - this->total = 1.0; } else if (this->values[0] < this->host->activeCPUs) { this->curAttributes = Medium_attributes; - this->total = this->host->activeCPUs; } else { this->curAttributes = High_attributes; - this->total = 2 * this->host->activeCPUs; } xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%.2f/%.2f/%.2f", this->values[0], this->values[1], this->values[2]); @@ -78,15 +78,15 @@ static void LoadMeter_updateValues(Meter* this) { Platform_getLoadAverage(&this->values[0], &five, &fifteen); // change bar color and total based on value + if (this->total < this->host->activeCPUs) { + this->total = this->host->activeCPUs; + } if (this->values[0] < 1.0) { this->curAttributes = OK_attributes; - this->total = 1.0; } else if (this->values[0] < this->host->activeCPUs) { this->curAttributes = Medium_attributes; - this->total = this->host->activeCPUs; } else { this->curAttributes = High_attributes; - this->total = 2 * this->host->activeCPUs; } xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%.2f", this->values[0]); @@ -111,7 +111,8 @@ const MeterClass LoadAverageMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, - .total = 100.0, + .isPercentChart = false, + .total = 1.0, .attributes = LoadAverageMeter_attributes, .name = "LoadAverage", .uiName = "Load average", @@ -129,7 +130,8 @@ const MeterClass LoadMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 1, - .total = 100.0, + .isPercentChart = false, + .total = 1.0, .attributes = LoadMeter_attributes, .name = "Load", .uiName = "Load", diff --git a/MemoryMeter.c b/MemoryMeter.c index 3b7835256..83788b7ee 100644 --- a/MemoryMeter.c +++ b/MemoryMeter.c @@ -111,6 +111,7 @@ const MeterClass MemoryMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = MEMORY_METER_ITEMCOUNT, + .isPercentChart = true, .total = 100.0, .attributes = MemoryMeter_attributes, .name = "Memory", diff --git a/Meter.c b/Meter.c index 4463a90a0..946d58803 100644 --- a/Meter.c +++ b/Meter.c @@ -10,7 +10,9 @@ in the source distribution for its full text. #include "Meter.h" #include +#include #include +#include #include #include @@ -24,11 +26,51 @@ in the source distribution for its full text. #include "XUtils.h" +#ifndef UINT16_WIDTH +#define UINT16_WIDTH 16 +#endif + #ifndef UINT32_WIDTH #define UINT32_WIDTH 32 #endif -#define GRAPH_HEIGHT 4 /* Unit: rows (lines) */ +#define DEFAULT_GRAPH_HEIGHT 4 /* Unit: rows (lines) */ +#define MAX_GRAPH_HEIGHT 8191 /* == (int)(UINT16_MAX / 8) */ + +typedef struct GraphColorCell_ { + uint8_t itemNum; + uint8_t details; +} GraphColorCell; + +typedef union GraphDataCell_ { + int16_t scaleExp; + uint16_t numDots; + GraphColorCell c; +} GraphDataCell; + +typedef struct GraphDrawContext_ { + uint8_t maxItems; + bool isPercentChart; + size_t nCellsPerValue; +} GraphDrawContext; + +typedef struct GraphColorAdjStack_ { + double startPoint; + double fractionSum; + double valueSum; + uint8_t nItems; +} GraphColorAdjStack; + +typedef struct GraphColorAdjOffset_ { + uint32_t offsetVal; /* "offsetVal" requires at least 22 bits */ + unsigned int nCells; +} GraphColorAdjOffset; + +typedef struct GraphColorComputeState_ { + double valueSum; + unsigned int nCellsPainted; + uint8_t nItemsPainted; +} GraphColorComputeState; typedef struct MeterMode_ { Meter_Draw draw; @@ -46,6 +88,14 @@ static inline void Meter_displayBuffer(const Meter* this, RichString* out) { } } +static double Meter_computeSum(const Meter* this) { + assert(this->curItems > 0); + assert(this->values); + double sum = sumPositiveValues(this->values, this->curItems); + // Prevent rounding to infinity in IEEE 754 + return MINIMUM(DBL_MAX, sum); +} + /* ---------- TextMeterMode ---------- */ static void TextMeterMode_draw(Meter* this, int x, int y, int w) { @@ -120,6 +170,14 @@ static void BarMeterMode_draw(Meter* this, int x, int y, int w) { assert(startPos <= w); assert(startPos + w <= RichString_sizeVal(bar)); + // Update the "total" if necessary + if (!Meter_isPercentChart(this) && this->curItems > 0) { + double sum = Meter_computeSum(this); + if (this->total < sum) { + this->total = sum; + } + } + int blockSizes[10]; // First draw in the bar[] buffer... @@ -169,25 +227,896 @@ static void BarMeterMode_draw(Meter* this, int x, int y, int w) { /* ---------- GraphMeterMode ---------- */ -#ifdef HAVE_LIBNCURSESW +static void GraphMeterMode_reallocateGraphBuffer(Meter* this, const GraphDrawContext* context, size_t nValues) { + GraphData* data = &this->drawData; -#define PIXPERROW_UTF8 4 -static const char* const GraphMeterMode_dotsUtf8[] = { - /*00*/" ", /*01*/"⢀", /*02*/"⢠", /*03*/"⢰", /*04*/ "⢸", - /*10*/"⡀", /*11*/"⣀", /*12*/"⣠", /*13*/"⣰", /*14*/ "⣸", - /*20*/"⡄", /*21*/"⣄", /*22*/"⣤", /*23*/"⣴", /*24*/ "⣼", - /*30*/"⡆", /*31*/"⣆", /*32*/"⣦", /*33*/"⣶", /*34*/ "⣾", - /*40*/"⡇", /*41*/"⣇", /*42*/"⣧", /*43*/"⣷", /*44*/ "⣿" -}; + size_t nCellsPerValue = context->nCellsPerValue; + size_t valueSize = nCellsPerValue * sizeof(GraphDataCell); -#endif + if (!valueSize) + goto bufferInitialized; -#define PIXPERROW_ASCII 2 -static const char* const GraphMeterMode_dotsAscii[] = { - /*00*/" ", /*01*/".", /*02*/":", - /*10*/".", /*11*/".", /*12*/":", - /*20*/":", /*21*/":", /*22*/":" -}; + data->buffer = xReallocArray(data->buffer, nValues, valueSize); + + // Move existing records ("values") to correct position + assert(nValues >= data->nValues); + size_t moveOffset = (nValues - data->nValues) * nCellsPerValue; + GraphDataCell* dest = &((GraphDataCell*)data->buffer)[moveOffset]; + memmove(dest, data->buffer, data->nValues * valueSize); + + // Fill new spaces with blank records + memset(data->buffer, 0, moveOffset * sizeof(GraphDataCell)); + +bufferInitialized: + data->nValues = nValues; +} + +#if 0 /* Unused code */ +static size_t GraphMeterMode_valueCellIndex(unsigned int graphHeight, bool isPercentChart, int deltaExp, unsigned int y, unsigned int* scaleFactor, unsigned int* increment) { + assert(deltaExp >= 0); + assert(deltaExp < UINT16_WIDTH); + + if (scaleFactor) + *scaleFactor = 1; + if (increment) + *increment = isPercentChart ? 1 : (2U << deltaExp); + + unsigned int yTop = (graphHeight - 1) >> deltaExp; + if (y > yTop) { + return (size_t)-1; + } + + if (isPercentChart) { + assert(deltaExp == 0); + return y; + } + + // A record may be rendered in different scales depending on the largest + // "scaleExp" value of a record set. The colors are precomputed for + // different scales of the same record. It takes (2 * graphHeight - 1) cells + // of space to store all the color information. + // + // An example for graphHeight = 6: + // + // scale 1*n 2*n 4*n 8*n 16*n | n = value sum of all items + // --------------------------------- | rounded up to a power of + // deltaExp 0 1 2 3 4 | two. The exponent of n is + // --------------------------------- | stored in index [0]. + // array [11] X X X X | X = empty cell + // indices [9] X X X X | Cells whose array indices + // [7] X X X X | are >= (2 * graphHeight) are + // [5] [10] X X X | computed from cells of a + // [3] [6] (12) X X | lower scale and not stored in + // [1] [2] [4] [8] (16) | the array. + + // "b" is the "base" offset or the upper bits of offset + unsigned int b = (y * 2) << deltaExp; + unsigned int offset = 1U << deltaExp; + + if (!scaleFactor) { + // This function is called for writing. The "increment" argument is + // optional, but the caller should assert the index is in bounds. + if (b + offset > 2 * graphHeight - 1) { + return (size_t)-1; + } + return b + offset; + } + + // This function is called for reading. + assert(!increment); + + if (y < yTop) { + return b + offset; + } + + assert(((2 * graphHeight - 1) & b) == b); + + unsigned int offsetTop = powerOf2Floor(2 * graphHeight - 1 - b); + if (offsetTop) { + *scaleFactor = offset / offsetTop; + } + + return b + offsetTop; +} + +static uint8_t GraphMeterMode_findTopCellItem(const Meter* this, double scaledTotal, unsigned int topCell) { + unsigned int graphHeight = (unsigned int)this->h; + assert(topCell < graphHeight); + + double valueSum = 0.0; + double maxValue = 0.0; + uint8_t topCellItem = this->curItems - 1; + for (uint8_t i = 0; i < this->curItems && valueSum < DBL_MAX; i++) { + double value = this->values[i]; + if (!isPositive(value)) + continue; + + double newValueSum = valueSum + value; + if (newValueSum > DBL_MAX) + newValueSum = DBL_MAX; + + if (value > DBL_MAX - valueSum) { + value = DBL_MAX - valueSum; + // This assumption holds for the new "value" as long as the + // rounding mode is consistent. + assert(newValueSum < DBL_MAX || valueSum + value >= DBL_MAX); + } + + valueSum = newValueSum; + + // Find the item that occupies the largest area of the top cell. + // Favor the item with higher index in case of a tie. + + if (topCell > 0) { + double topPoint = (valueSum / scaledTotal) * (double)(int)graphHeight; + assert(topPoint >= 0.0); + + if (!(topPoint > (double)(int)topCell)) + continue; + + // This code assumes the default FP rounding mode (i.e. to nearest), + // which requires "area" to be at least (DBL_EPSILON / 2) to win. + + double area = (value / scaledTotal) * (double)(int)graphHeight; + assert(area >= 0.0); + + if (area > topPoint - (double)(int)topCell) + area = topPoint - (double)(int)topCell; + + if (area >= maxValue) { + maxValue = area; + topCellItem = i; + } + } else { + // Compare "value" directly. It is possible for an "area" to + // underflow here and still wins as the largest area. + if (value >= maxValue) { + maxValue = value; + topCellItem = i; + } + } + } + return topCellItem; +} + +static int8_t GraphMeterMode_needsExtraCell(unsigned int graphHeight, double scaledTotal, unsigned int y, const GraphColorAdjStack* stack, const GraphColorAdjOffset* adjOffset) { + double areaSum = (stack->fractionSum + stack->valueSum / scaledTotal) * (double)(int)graphHeight; + double adjOffsetVal = adjOffset ? (double)(int32_t)adjOffset->offsetVal : 0.0; + double halfPoint = (double)(int)y + 0.5; + + // Calculate the best position for rendering this stack of items. + // Given real numbers a, b, c and d (a <= b <= c <= d), then: + // 1. The smallest value for (x - a)^2 + (x - b)^2 + (x - c)^2 + (x - d)^2 + // happens when x == (a + b + c + d) / 4; x is the "arithmetic mean". + // 2. The smallest value for |y - a| + |y - b| + |y - c| + |y - d| + // happens when b <= y <= c; y is the "median". + // Both kinds of averages are acceptable. The arithmetic mean is chosen + // here because it is cheaper to produce. + + // averagePoint := stack->startPoint + (areaSum / (stack->nItems * 2)) + // adjStartPoint := averagePoint - (adjOffsetVal / (stack->nItems * 2)) + + // Intended to compare this but with greater precision: + // isgreater(adjStartPoint, halfPoint) + if (areaSum - adjOffsetVal > (halfPoint - stack->startPoint) * 2.0 * stack->nItems) + return 1; + + if (areaSum - adjOffsetVal < (halfPoint - stack->startPoint) * 2.0 * stack->nItems) + return 0; + + assert(stack->valueSum <= DBL_MAX); + double stackArea = (stack->valueSum / scaledTotal) * (double)(int)graphHeight; + double adjNCells = adjOffset ? (double)(int)adjOffset->nCells : 0.0; + + // Intended to compare this but with greater precision: + // (stack->startPoint + (stackArea / 2) > halfPoint + (adjNCells / 2)) + if (stackArea - adjNCells > (halfPoint - stack->startPoint) * 2.0) + return 1; + + if (stackArea - adjNCells < (halfPoint - stack->startPoint) * 2.0) + return 0; + + return -1; +} + +static void GraphMeterMode_addItemAdjOffset(GraphColorAdjOffset* adjOffset, unsigned int nCells) { + adjOffset->offsetVal += (uint32_t)adjOffset->nCells * 2 + nCells; + adjOffset->nCells += nCells; +} + +static void GraphMeterMode_addItemAdjStack(GraphColorAdjStack* stack, double scaledTotal, double value) { + assert(scaledTotal <= DBL_MAX); + assert(stack->valueSum < DBL_MAX); + + stack->fractionSum += (stack->valueSum / scaledTotal) * 2.0; + stack->valueSum += value; + + assert(stack->nItems < UINT8_MAX); + stack->nItems++; +} + +static uint16_t GraphMeterMode_makeDetailsMask(const GraphColorComputeState* prev, const GraphColorComputeState* new, double prevTopPoint, double rem, int blanksAtTopCell) { + assert(new->nCellsPainted > prev->nCellsPainted); + assert(rem >= 0.0); + assert(rem < 1.0); + + double numDots = ceil(rem * 8.0); + + uint8_t blanksAtEnd; + int8_t roundDirInAscii = 0; + if (blanksAtTopCell >= 0) { + assert(blanksAtTopCell < 8); + blanksAtEnd = (uint8_t)blanksAtTopCell; + roundDirInAscii = 1; + } else if (prev->nCellsPainted == 0 || prevTopPoint <= (double)(int)prev->nCellsPainted || (uint8_t)numDots == 0) { + blanksAtEnd = (uint8_t)(8 - (uint8_t)numDots) % 8; + } else { + // Unlike other conditions, this one rounds to nearest for visual reason. + // In case of a tie, display the dot at lower position of the graph, + // i.e. MSB of the "details" data. + + double distance = prevTopPoint - (double)(int)prev->nCellsPainted; + distance = distance + rem * 0.5; + + // Tiebreaking direction that may be needed in the ASCII display mode. + if (distance > 0.5) { + roundDirInAscii = 1; + } else if (distance < 0.5) { + roundDirInAscii = -1; + } + + distance *= 8.0; + if ((uint8_t)numDots % 2 == 0) { + distance -= 0.5; + } + distance = ceil(distance); + assert(distance >= 0.0); + assert(distance < INT_MAX); + + unsigned int blanksRem = 8 - (unsigned int)(int)numDots / 2; + blanksRem -= MINIMUM(blanksRem, (unsigned int)(int)distance); + blanksAtEnd = (uint8_t)blanksRem; + } + assert(blanksAtEnd < 8); + + uint8_t blanksAtStart; + if (prev->nCellsPainted > 0) { + blanksAtStart = (uint8_t)(8 - (uint8_t)numDots - blanksAtEnd) % 8; + } else { + // Always zero blanks for the first cell. + // When an item would be painted with all cells (from the first cell to + // the "top cell"), it is expected that the bar would be "stretched" to + // represent the sum of the record. + blanksAtStart = 0; + } + assert(blanksAtStart < 8); + + uint16_t mask = 0xFFFFU >> blanksAtStart; + // See the code and comments of the "printCellDetails" function for how + // special bits are used. + bool needsTiebreak = blanksAtStart >= 2 && blanksAtStart < 4 && blanksAtStart == blanksAtEnd; + + if (new->nCellsPainted - prev->nCellsPainted == 1) { + assert(blanksAtStart + blanksAtEnd < 8); + if (roundDirInAscii > 0 && needsTiebreak) { + assert((mask & 0x0800) != 0); + mask ^= 0x0800; + } + mask >>= 8; + } else if (roundDirInAscii > 0) { + if (blanksAtStart < 4 && (uint8_t)(blanksAtStart + blanksAtEnd % 4) >= 4) { + assert((mask & 0x0800) != 0); + mask ^= 0x0800; + } + } + + mask = (uint16_t)((mask >> blanksAtEnd) << blanksAtEnd); + + if (needsTiebreak) { + if (roundDirInAscii > 0) { + mask |= 0x0004; + } else if (roundDirInAscii < 0) { + assert((mask & 0x0010) != 0); + mask = (mask & 0xFFEF) | 0x0020; + } + } else if (roundDirInAscii < 0) { + assert(blanksAtStart <= blanksAtEnd); + if ((mask | 0x4000) == 0x7FF8) { + // This special case is the combination of the 4 conditionals, + // shown as asserts below. + assert(new->nCellsPainted - prev->nCellsPainted > 1); + assert(blanksAtEnd < 4); + assert(blanksAtStart % 4 + blanksAtEnd >= 4); + assert(blanksAtStart < blanksAtEnd); + + mask = (mask & 0xFFEF) | 0x0020; + } + } + + // The following result values are impossible as they lack special bits + // needed for the ASCII display mode. + assert(mask != 0x3FF8); // Should be 0x37F8 or 0x3FE8 + assert(mask != 0x7FF8); // Should be 0x77F8 or 0x7FE8 + assert(mask != 0x1FFC); // Should be 0x17FC + assert(mask != 0x1FFE); // Should be 0x17FE + + return mask; +} + +static void GraphMeterMode_paintCellsForItem(GraphDataCell* cellsStart, unsigned int increment, uint8_t itemIndex, unsigned int nCells, uint16_t mask) { + GraphDataCell* cell = cellsStart; + while (nCells > 0) { + cell->c.itemNum = itemIndex + 1; + if (nCells == 1) { + cell->c.details = (uint8_t)mask; + } else if (cell == cellsStart) { + cell->c.details = mask >> 8; + } else { + cell->c.details = 0xFF; + } + nCells--; + cell += increment; + } +} + +static void GraphMeterMode_computeColors(Meter* this, const GraphDrawContext* context, GraphDataCell* valueStart, int deltaExp, double scaledTotal, unsigned int numDots) { + unsigned int graphHeight = (unsigned int)this->h; + bool isPercentChart = context->isPercentChart; + + assert(deltaExp >= 0); + assert(numDots > 0); + assert(numDots <= graphHeight * 8); + + unsigned int increment; + size_t firstCellIndex = GraphMeterMode_valueCellIndex(graphHeight, isPercentChart, deltaExp, 0, NULL, &increment); + assert(firstCellIndex < context->nCellsPerValue); + + unsigned int topCell = (numDots - 1) / 8; + const uint8_t dotAlignment = 2; + unsigned int blanksAtTopCell = ((topCell + 1) * 8 - numDots) / dotAlignment * dotAlignment; + + bool hasPartialTopCell = false; + if (blanksAtTopCell > 0) { + hasPartialTopCell = true; + } else if (!isPercentChart && topCell % 2 == 0 && topCell == ((graphHeight - 1) >> deltaExp)) { + // This "top cell" is rendered as full in one scale, but partial in the + // next scale. (Only happens when graphHeight is not a power of two.) + hasPartialTopCell = true; + } + + double topCellArea = 0.0; + assert(this->curItems > 0); + uint8_t topCellItem = this->curItems - 1; + if (hasPartialTopCell) { + // Allocate the "top cell" first. The item that acquires the "top cell" + // will have a smaller "area" for the remainder calculation below. + topCellArea = (8 - (int)blanksAtTopCell) / 8.0; + topCellItem = GraphMeterMode_findTopCellItem(this, scaledTotal, topCell); + } + + GraphColorComputeState restart = { + .valueSum = 0.0, + .nCellsPainted = 0, + .nItemsPainted = 0 + }; + double thresholdHigh = 1.0; + double thresholdLow = 0.0; + double threshold = 0.5; + bool rItemIsDetermined = false; + bool rItemHasExtraCell = true; + unsigned int nCellsToPaint = topCell + 1; + bool isLastTiebreak = false; + unsigned int nCellsPaintedHigh = nCellsToPaint + topCellItem + 1; + unsigned int nCellsPaintedLow = 0; + + while (true) { + GraphColorComputeState prev = restart; + double nextThresholdLow = thresholdHigh; + double nextThresholdHigh = thresholdLow; + bool hasThresholdRange = thresholdLow < thresholdHigh; + GraphColorAdjStack stack = { + .startPoint = 0.0, + .fractionSum = 0.0, + .valueSum = 0.0, + .nItems = 0 + }; + GraphColorAdjOffset adjSmall = { + .offsetVal = 0, + .nCells = 0 + }; + GraphColorAdjOffset adjLarge = adjSmall; + + while (prev.nItemsPainted <= topCellItem && prev.valueSum < DBL_MAX) { + double value = this->values[prev.nItemsPainted]; + if (!isPositive(value)) { + if (restart.nItemsPainted == prev.nItemsPainted) { + restart.nItemsPainted++; + } + prev.nItemsPainted++; + continue; + } + + GraphColorComputeState new; + + new.valueSum = prev.valueSum + value; + if (new.valueSum > DBL_MAX) + new.valueSum = DBL_MAX; + + if (value > DBL_MAX - prev.valueSum) { + value = DBL_MAX - prev.valueSum; + // This assumption holds for the new "value" as long as the + // rounding mode is consistent. + assert(new.valueSum < DBL_MAX || prev.valueSum + value >= DBL_MAX); + } + + double area = (value / scaledTotal) * (double)(int)graphHeight; + assert(area >= 0.0); // "area" can be 0.0 when the division underflows + double rem = area; + + if (prev.nItemsPainted == topCellItem) + rem = MAXIMUM(area, topCellArea) - topCellArea; + + unsigned int nCells = (unsigned int)(int)rem; + rem -= (int)rem; + + // Whether the item will receive an extra cell or be truncated. + // The main method is known as the "largest remainder method". + + // An item whose remainder reaches the Droop quota may either receive + // an extra cell or need a tiebreak (a tie caused by rounding). + // This is the highest threshold we might need to compare with. + bool reachesDroopQuota = rem * (double)(int)(graphHeight + 1) > (double)(int)graphHeight; + if (reachesDroopQuota && rem < thresholdHigh) + thresholdHigh = rem; + + bool equalsThreshold = false; + bool isInThresholdRange = rem <= thresholdHigh && rem >= thresholdLow; + + assert(threshold > 0.0); + assert(threshold <= 1.0); + if (rem > threshold) { + if (rem < nextThresholdLow) { + nextThresholdLow = rem; + } + nCells++; + } else if (rem < threshold) { + if (rem > nextThresholdHigh) { + nextThresholdHigh = rem; + } + rem = 0.0; + } else if (hasThresholdRange) { + assert(!rItemIsDetermined); + nCells++; + } else if (restart.nItemsPainted >= prev.nItemsPainted) { + assert(restart.nItemsPainted == prev.nItemsPainted); + + // This item will be nicknamed "rItem". Whether the "rItem" will + // receive an extra cell is determined by the rest of the loop. + if (!rItemIsDetermined) { + stack.startPoint = (new.valueSum / scaledTotal) * (double)(int)graphHeight; + rem = 0.0; + } else if (rItemHasExtraCell) { + nCells++; + } else { + rem = 0.0; + } + } else { + equalsThreshold = true; + rem = 0.0; + + unsigned int y = prev.nCellsPainted - adjSmall.nCells; + unsigned int rItemMinCells = y - restart.nCellsPainted; + + // The first cell and last cell are painted with dots aligned to the + // bottom and top respectively. If multiple items whose remainders + // equal the threshold and would be painted on the same cell, give + // priority to the first or last of the items respectively. + + if (prev.nCellsPainted == 0) { + assert(adjSmall.nCells == 0); + rItemHasExtraCell = true; + } else if (y + 1 >= nCellsToPaint) { + assert(y + 1 == nCellsToPaint); + assert(adjSmall.nCells == 0); + assert(nCells == 0); + rItemHasExtraCell = false; + } else if (!rItemHasExtraCell) { + assert(adjLarge.nCells > adjSmall.nCells); + + int8_t res = GraphMeterMode_needsExtraCell(graphHeight, scaledTotal, y, &stack, &adjLarge); + if (res > 0 || (res < 0 && rItemMinCells <= nCells)) { + rItemHasExtraCell = true; + } + } else { + int8_t res = GraphMeterMode_needsExtraCell(graphHeight, scaledTotal, y, &stack, &adjSmall); + if (res == 0 || (res < 0 && (rItemMinCells > nCells || prev.nCellsPainted + 1 >= nCellsToPaint))) { + rItemHasExtraCell = false; + } + } + } + + if (!hasThresholdRange && restart.nItemsPainted < prev.nItemsPainted) { + GraphMeterMode_addItemAdjOffset(&adjSmall, nCells); + GraphMeterMode_addItemAdjOffset(&adjLarge, nCells + equalsThreshold); + GraphMeterMode_addItemAdjStack(&stack, scaledTotal, value); + } + + if (hasPartialTopCell && prev.nItemsPainted == topCellItem) + nCells++; + + new.nCellsPainted = prev.nCellsPainted + nCells; + new.nItemsPainted = prev.nItemsPainted + 1; + + // Update the "restart" state if needed + if (restart.nItemsPainted >= prev.nItemsPainted) { + if (!isInThresholdRange) { + restart = new; + } else if (rItemIsDetermined) { + restart = new; + rItemIsDetermined = isLastTiebreak; + rItemHasExtraCell = true; + } + } + + // Paint cells to the buffer + if (hasPartialTopCell && prev.nItemsPainted == topCellItem) { + // Re-calculate the remainder with the top cell area included + if (rem > 0.0) { + // Has extra cell won from the largest remainder method + rem = area; + } else { + // Did not win extra cell from the remainder + rem = MINIMUM(area, topCellArea); + } + rem -= (int)rem; + } + + bool isItemOnEdge = (prev.nCellsPainted == 0 || new.nCellsPainted == nCellsToPaint); + if (isItemOnEdge && area < (0.125 * dotAlignment)) + rem = (0.125 * dotAlignment); + + if (nCells > 0 && new.nCellsPainted <= nCellsToPaint) { + double prevTopPoint = (prev.valueSum / scaledTotal) * (double)(int)graphHeight; + int blanksAtTopCellArg = (new.nCellsPainted == nCellsToPaint) ? (int)blanksAtTopCell : -1; + uint16_t mask = GraphMeterMode_makeDetailsMask(&prev, &new, prevTopPoint, rem, blanksAtTopCellArg); + + GraphDataCell* cellsStart = &valueStart[firstCellIndex + (size_t)increment * prev.nCellsPainted]; + GraphMeterMode_paintCellsForItem(cellsStart, increment, prev.nItemsPainted, nCells, mask); + } + + prev = new; + } + + if (hasThresholdRange) { + if (prev.nCellsPainted == nCellsToPaint) + break; + + // Set new threshold range + if (prev.nCellsPainted > nCellsToPaint) { + nCellsPaintedHigh = prev.nCellsPainted; + assert(thresholdLow < threshold); + thresholdLow = threshold; + } else { + nCellsPaintedLow = prev.nCellsPainted + 1; + assert(thresholdHigh > nextThresholdHigh); + thresholdHigh = nextThresholdHigh; + nextThresholdLow = thresholdLow; + } + + // Make new threshold value + threshold = thresholdHigh; + hasThresholdRange = thresholdLow < thresholdHigh; + if (hasThresholdRange && nCellsPaintedLow < nCellsPaintedHigh) { + // Linear interpolation + assert(nCellsPaintedLow <= nCellsToPaint); + threshold -= ((thresholdHigh - thresholdLow) * (double)(int)(nCellsToPaint - nCellsPaintedLow) / (double)(int)(nCellsPaintedHigh - nCellsPaintedLow)); + if (threshold < nextThresholdLow) { + threshold = nextThresholdLow; + } + } + assert(threshold <= thresholdHigh); + } else if (restart.nItemsPainted <= topCellItem && restart.valueSum < DBL_MAX) { + if (prev.nCellsPainted - adjSmall.nCells + adjLarge.nCells < nCellsToPaint) { + rItemHasExtraCell = true; + isLastTiebreak = true; + } else if (prev.nCellsPainted >= nCellsToPaint) { + assert(prev.nCellsPainted == nCellsToPaint); + break; + } + rItemIsDetermined = true; + } else { + assert(restart.nCellsPainted == nCellsToPaint); + break; + } + } +} +#endif /* End of unused code */ + +static void GraphMeterMode_recordNewValue(Meter* this, const GraphDrawContext* context) { + bool isPercentChart = context->isPercentChart; + size_t nCellsPerValue = context->nCellsPerValue; + if (!nCellsPerValue) + return; + + GraphData* data = &this->drawData; + size_t nValues = data->nValues; + unsigned int graphHeight = (unsigned int)this->h; + + // Move previous records + size_t valueSize = nCellsPerValue * sizeof(GraphDataCell); + GraphDataCell* valueStart = (GraphDataCell*)data->buffer; + valueStart = &valueStart[1 * nCellsPerValue]; + memmove(data->buffer, valueStart, (nValues - 1) * valueSize); + + valueStart = (GraphDataCell*)data->buffer; + valueStart = &valueStart[(nValues - 1) * nCellsPerValue]; + + // Compute "sum" and "total" + double total = 0.0; + if (isPercentChart) { + assert(this->total >= 0.0); + total = this->total; + valueStart[0].scaleExp = 0; + } else { + if (isPositive(this->values[0])) + total = this->values[0]; + + if (this->curItems > 1 && isgreater(this->values[1], total)) + total = this->values[1]; + + if (total > DBL_MAX) + total = DBL_MAX; + + int scaleExp = 0; + (void)frexp(total, &scaleExp); + if (scaleExp < 0) { + scaleExp = 0; + } + // In IEEE 754 binary64 (DBL_MAX_EXP == 1024, DBL_MAX_10_EXP == 308), + // "scaleExp" never overflows. + assert(DBL_MAX_10_EXP < 9864); + assert(scaleExp <= INT16_MAX); + valueStart[0].scaleExp = (int16_t)scaleExp; + total = ldexp(1.0, scaleExp); + } + if (total > DBL_MAX) + total = DBL_MAX; + + assert(graphHeight <= UINT16_MAX / 8); + double maxDots = (double)(int32_t)(graphHeight * 8); + for (uint8_t i = 0; i < 2; i++) { + double value = 0.0; + if (i < this->curItems && isPositive(this->values[i])) + value = this->values[i]; + + if (value > total) + value = total; + + int numDots = 0; + if (total > 0.0) { + numDots = (int)ceil((value / total) * maxDots); + assert(numDots >= 0); + if (value > 0.0 && numDots <= 0) { + numDots = 1; // Division of (value / total) underflows + } + } + + assert(numDots <= UINT16_MAX); + valueStart[1 + i].numDots = (uint16_t)numDots; + } +} + +static void GraphMeterMode_printScale(int exponent) { + if (exponent < 10) { + // "1" to "512"; the (exponent < 0) case is not implemented. + assert(exponent >= 0); + printw("%3u", 1U << exponent); + } else if (exponent > (int)ARRAYSIZE(unitPrefixes) * 10 + 6) { + addstr("inf"); + } else if (exponent % 10 < 7) { + // "1K" to "64K", "1M" to "64M", "1G" to "64G", etc. + printw("%2u%c", 1U << (exponent % 10), unitPrefixes[exponent / 10 - 1]); + } else { + // "M/8" (=128K), "M/4" (=256K), "M/2" (=512K), "G/8" (=128M), etc. + printw("%c/%u", unitPrefixes[exponent / 10], 1U << (10 - exponent % 10)); + } +} + +#if 0 /* Unused code */ +static uint8_t GraphMeterMode_scaleCellDetails(uint8_t details, unsigned int scaleFactor) { + // Only the "top cell" of a record may need scaling like this; the cell does + // not use the special meaning of bit 4. + // This algorithm assumes the "details" be printed in braille characters. + assert(scaleFactor > 0); + if (scaleFactor < 2) { + return details; + } + if (scaleFactor < 4 && (details & 0x0F) != 0x00) { + // Display the cell in half height (bits 0 to 3 are zero). + // Bits 4 and 5 are set simultaneously to avoid a jaggy visual. + uint8_t newDetails = 0x30; + // Bit 6 + if (popCount8(details) > 4) + newDetails |= 0x40; + // Bit 7 (equivalent to (details >= 0x80 || popCount8(details) > 6)) + if (details >= 0x7F) + newDetails |= 0x80; + return newDetails; + } + if (details != 0x00) { + // Display the cell in a quarter height (bits 0 to 5 are zero). + // Bits 6 and 7 are set simultaneously. + return 0xC0; + } + return 0x00; +} +#endif /* End of unused code */ + +static int GraphMeterMode_lookupCell(const Meter* this, const GraphDrawContext* context, int scaleExp, size_t valueIndex, unsigned int y, uint8_t* details) { + unsigned int graphHeight = (unsigned int)this->h; + const GraphData* data = &this->drawData; + + uint8_t maxItems = context->maxItems; + bool isPercentChart = context->isPercentChart; + size_t nCellsPerValue = context->nCellsPerValue; + + // Reverse the coordinate + assert(y < graphHeight); + y = graphHeight - 1 - y; + + uint8_t itemIndex = (uint8_t)-1; + *details = 0x00; // Empty the cell + + if (maxItems < 1) + goto cellIsEmpty; + + assert(valueIndex < data->nValues); + const GraphDataCell* valueStart = (const GraphDataCell*)data->buffer; + valueStart = &valueStart[valueIndex * nCellsPerValue]; + + int deltaExp = isPercentChart ? 0 : scaleExp - valueStart[0].scaleExp; + assert(deltaExp >= 0); + + unsigned int numDots = valueStart[1].numDots; + if (numDots >= 1) { + if (deltaExp + 1 < UINT16_WIDTH) { + numDots = ((numDots - 1) >> (deltaExp + 1)) + 1; + } else { + numDots = 1; + } + } + unsigned int blanksAtEnd = graphHeight * 4 - numDots; + + numDots = valueStart[2].numDots; + if (numDots >= 1) { + if (deltaExp + 1 < UINT16_WIDTH) { + numDots = ((numDots - 1) >> (deltaExp + 1)) + 1; + } else { + numDots = 1; + } + } + unsigned int blanksAtStart = graphHeight * 4 - numDots; + + if (graphHeight - 1 - y < blanksAtEnd / 8) + goto cellIsEmpty; + if (y < blanksAtStart / 8) + goto cellIsEmpty; + + if (y * 2 == graphHeight - 1 && !(valueStart[1].numDots || valueStart[2].numDots)) + goto cellIsEmpty; + + if (maxItems <= 1 || y * 2 > graphHeight - 1) { + itemIndex = 0; + } else if (y * 2 < graphHeight - 1) { + itemIndex = 1; + } else { + itemIndex = valueStart[1].numDots >= valueStart[2].numDots ? 0 : 1; + } + + if (y * 2 == graphHeight - 1 && valueStart[1].numDots > 8 && valueStart[2].numDots > 8) { + *details = valueStart[1].numDots >= valueStart[2].numDots ? 0x0F : 0xF0; + } else { + *details = 0xFF; + const uint8_t dotAlignment = 2; + if (y == blanksAtStart / 8) { + blanksAtStart = (blanksAtStart % 8) / dotAlignment * dotAlignment; + *details >>= blanksAtStart; + } + if ((graphHeight - 1 - y) == blanksAtEnd / 8) { + blanksAtEnd = (blanksAtEnd % 8) / dotAlignment * dotAlignment; + *details = (uint8_t)((*details >> blanksAtEnd) << blanksAtEnd); + } + if (*details == 0x3C) { + *details = 0x24; + } + } + /* fallthrough */ + +cellIsEmpty: + if (itemIndex == (uint8_t)-1) + return BAR_SHADOW; + + assert(itemIndex < maxItems); + return Meter_attributes(this)[itemIndex]; +} + +static void GraphMeterMode_printCellDetails(uint8_t details) { + if (details == 0x00) { + // Use ASCII space instead. A braille blank character may display as a + // substitute block and is less distinguishable from a cell with data. + addch(' '); + return; + } +#ifdef HAVE_LIBNCURSESW + if (CRT_utf8) { + // Bits 3 and 4 of "details" might carry special meaning. When the whole + // byte contains specific bit patterns, it indicates that only half cell + // should be displayed in the ASCII display mode. The bits are supposed + // to be filled in the Unicode display mode. + if ((details & 0x9C) == 0x14 || (details & 0x39) == 0x28 || details == 0x24) { + if (details == 0x14 || details == 0x28) { // Special case + details = 0x18; + } else { + details |= 0x18; + } + } + // Convert GraphDataCell.c.details bit representation to Unicode braille + // dot ordering. + // (Bit0) a b (Bit3) From: h g f e d c b a (binary) + // (Bit1) c d (Bit4) | | | X X | + // (Bit2) e f (Bit5) | | | | \ / | | + // (Bit6) g h (Bit7) | | | | X | | + // To: 0x2800 + h g f d b e c a + // Braille Patterns [U+2800, U+28FF] in UTF-8: [E2 A0 80, E2 A3 BF] + char sequence[4] = "\xE2\xA0\x80"; + // Bits 6 and 7 are in the second byte of the UTF-8 sequence. + sequence[1] |= details >> 6; + // Bits 0 to 5 are in the third byte. + // The algorithm is optimized for x86 and ARM. + uint32_t n = details * 0x01010101U; + n = (uint32_t)((n & 0x08211204U) * 0x02110408U) >> 26; + sequence[2] |= n; + addstr(sequence); + return; + } +#endif + // ASCII display mode + const char upperHalf = '`'; + const char lowerHalf = '.'; + const char fullCell = ':'; + char c; + + // Detect special cases where we should print only half of the cell. + if ((details & 0x9C) == 0x14) { + c = upperHalf; + } else if ((details & 0x39) == 0x28) { + c = lowerHalf; + } else if (details == 0x24) { + c = fullCell; + // End of special cases + } else if (popCount8(details) > 4) { + c = fullCell; + } else { + // Determine which half has more dots than the other. + uint8_t inverted = details ^ 0x0F; + int difference = (int)popCount8(inverted) - 4; + if (difference < 0) { + c = upperHalf; + } else if (difference > 0) { + c = lowerHalf; + } else { + // Give weight to dots closer to the top or bottom of the cell (LSB or + // MSB, respectively) as a tiebreaker. + // Reverse bits 0 to 3 and subtract it from bits 4 to 7. + // The algorithm is optimized for x86 and ARM. + uint32_t n = inverted * 0x01010101U; + n = (uint32_t)((n & 0xF20508U) * 0x01441080U) >> 27; + difference = (int)n - 0x0F; + c = difference < 0 ? upperHalf : lowerHalf; + } + } + addch(c); +} static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { // Draw the caption @@ -195,20 +1124,38 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { attrset(CRT_colors[METER_TEXT]); const int captionLen = 3; mvaddnstr(y, x, caption, captionLen); + + // Prepare parameters for drawing + assert(this->h >= 1); + assert(this->h <= MAX_GRAPH_HEIGHT); + unsigned int graphHeight = (unsigned int)this->h; + + uint8_t maxItems = Meter_maxItems(this); + bool isPercentChart = Meter_isPercentChart(this); + size_t nCellsPerValue = maxItems < 1 ? maxItems : 3; + + GraphDrawContext context = { + .maxItems = maxItems, + .isPercentChart = isPercentChart, + .nCellsPerValue = nCellsPerValue + }; + + bool needsScaleDisplay = maxItems > 0 && graphHeight >= 2; + if (needsScaleDisplay) { + move(y + 1, x); // Cursor position for printing the scale + } x += captionLen; w -= captionLen; GraphData* data = &this->drawData; // Expand the graph data buffer if necessary - assert(data->nValues / 2 <= INT_MAX); - if (w > (int)(data->nValues / 2) && MAX_METER_GRAPHDATA_VALUES > data->nValues) { - size_t oldNValues = data->nValues; - data->nValues = MAXIMUM(oldNValues + oldNValues / 2, (size_t)w * 2); - data->nValues = MINIMUM(data->nValues, MAX_METER_GRAPHDATA_VALUES); - data->values = xReallocArray(data->values, data->nValues, sizeof(*data->values)); - memmove(data->values + (data->nValues - oldNValues), data->values, oldNValues * sizeof(*data->values)); - memset(data->values, 0, (data->nValues - oldNValues) * sizeof(*data->values)); + assert(data->nValues <= INT_MAX); + if (w > (int)data->nValues && MAX_METER_GRAPHDATA_VALUES > data->nValues) { + size_t nValues = data->nValues; + nValues = MAXIMUM(nValues + nValues / 2, (size_t)w); + nValues = MINIMUM(nValues, MAX_METER_GRAPHDATA_VALUES); + GraphMeterMode_reallocateGraphBuffer(this, &context, nValues); } const size_t nValues = data->nValues; @@ -222,54 +1169,46 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { struct timeval delay = { .tv_sec = globalDelay / 10, .tv_usec = (globalDelay % 10) * 100000L }; timeradd(&host->realtime, &delay, &(data->time)); - memmove(&data->values[0], &data->values[1], (nValues - 1) * sizeof(*data->values)); - - data->values[nValues - 1] = 0.0; - if (this->curItems > 0) { - assert(this->values); - data->values[nValues - 1] = sumPositiveValues(this->values, this->curItems); - } + GraphMeterMode_recordNewValue(this, &context); } if (w <= 0) return; - // Graph drawing style (character set, etc.) - const char* const* GraphMeterMode_dots; - int GraphMeterMode_pixPerRow; -#ifdef HAVE_LIBNCURSESW - if (CRT_utf8) { - GraphMeterMode_dots = GraphMeterMode_dotsUtf8; - GraphMeterMode_pixPerRow = PIXPERROW_UTF8; - } else -#endif - { - GraphMeterMode_dots = GraphMeterMode_dotsAscii; - GraphMeterMode_pixPerRow = PIXPERROW_ASCII; - } - // Starting positions of graph data and terminal column - if ((size_t)w > nValues / 2) { - x += w - nValues / 2; - w = nValues / 2; + if ((size_t)w > nValues) { + x += w - nValues; + w = (int)nValues; + } + size_t i = nValues - (size_t)w; + + // Determine and print the graph scale + int scaleExp = 0; + if (maxItems > 0 && !isPercentChart) { + for (size_t j = i; j < nValues; j++) { + const GraphDataCell* valueStart = (const GraphDataCell*)data->buffer; + valueStart = &valueStart[j * nCellsPerValue]; + if (scaleExp < valueStart[0].scaleExp) { + scaleExp = valueStart[0].scaleExp; + } + } + } + if (needsScaleDisplay) { + if (isPercentChart) { + addstr(" %"); + } else { + GraphMeterMode_printScale(scaleExp); + } } - size_t i = nValues - (size_t)w * 2; // Draw the actual graph - for (int col = 0; i < nValues - 1; i += 2, col++) { - int pix = GraphMeterMode_pixPerRow * GRAPH_HEIGHT; - double total = MAXIMUM(this->total, 1); - int v1 = (int) lround(CLAMP(data->values[i] / total * pix, 1.0, pix)); - int v2 = (int) lround(CLAMP(data->values[i + 1] / total * pix, 1.0, pix)); - - int colorIdx = GRAPH_1; - for (int line = 0; line < GRAPH_HEIGHT; line++) { - int line1 = CLAMP(v1 - (GraphMeterMode_pixPerRow * (GRAPH_HEIGHT - 1 - line)), 0, GraphMeterMode_pixPerRow); - int line2 = CLAMP(v2 - (GraphMeterMode_pixPerRow * (GRAPH_HEIGHT - 1 - line)), 0, GraphMeterMode_pixPerRow); - + for (unsigned int line = 0; line < graphHeight; line++) { + for (unsigned int col = 0; i + col < nValues; col++) { + uint8_t details; + int colorIdx = GraphMeterMode_lookupCell(this, &context, scaleExp, i + col, line, &details); + move(y + (int)line, x + (int)col); attrset(CRT_colors[colorIdx]); - mvaddstr(y + line, x + col, GraphMeterMode_dots[line1 * (GraphMeterMode_pixPerRow + 1) + line2]); - colorIdx = GRAPH_2; + GraphMeterMode_printCellDetails(details); } } attrset(CRT_colors[RESET_COLOR]); @@ -363,7 +1302,7 @@ static const MeterMode Meter_modes[] = { }, [GRAPH_METERMODE] = { .uiName = "Graph", - .h = GRAPH_HEIGHT, + .h = DEFAULT_GRAPH_HEIGHT, .draw = GraphMeterMode_draw, }, [LED_METERMODE] = { @@ -445,7 +1384,7 @@ void Meter_delete(Object* cast) { if (Meter_doneFn(this)) { Meter_done(this); } - free(this->drawData.values); + free(this->drawData.buffer); free(this->caption); free(this->values); free(this); @@ -475,8 +1414,8 @@ void Meter_setMode(Meter* this, MeterModeId modeIndex) { this->draw = Meter_drawFn(this); Meter_updateMode(this, modeIndex); } else { - free(this->drawData.values); - this->drawData.values = NULL; + free(this->drawData.buffer); + this->drawData.buffer = NULL; this->drawData.nValues = 0; const MeterMode* mode = &Meter_modes[modeIndex]; diff --git a/Meter.h b/Meter.h index b53a82324..0471b5b1b 100644 --- a/Meter.h +++ b/Meter.h @@ -75,6 +75,12 @@ typedef struct MeterClass_ { const char* const description; /* optional meter description in header setup menu */ const uint8_t maxItems; const bool isMultiColumn; /* whether the meter draws multiple sub-columns (defaults to false) */ + + /* Specifies how the meter is rendered in bar or graph mode: + true: a percent bar or graph with 'total' representing 100% or maximum. + false: the meter has no definite maximum; 'total' repesents initial + maximum value while actual maximum is updated automatically. */ + const bool isPercentChart; } MeterClass; #define As_Meter(this_) ((const MeterClass*)((this_)->super.klass)) @@ -94,12 +100,14 @@ typedef struct MeterClass_ { #define Meter_attributes(this_) As_Meter(this_)->attributes #define Meter_name(this_) As_Meter(this_)->name #define Meter_uiName(this_) As_Meter(this_)->uiName +#define Meter_maxItems(this_) As_Meter(this_)->maxItems #define Meter_isMultiColumn(this_) As_Meter(this_)->isMultiColumn +#define Meter_isPercentChart(this_) As_Meter(this_)->isPercentChart typedef struct GraphData_ { struct timeval time; size_t nValues; - double* values; + void* buffer; } GraphData; struct Meter_ { diff --git a/NetworkIOMeter.c b/NetworkIOMeter.c index da3ce71da..3f4acdea5 100644 --- a/NetworkIOMeter.c +++ b/NetworkIOMeter.c @@ -111,9 +111,6 @@ static void NetworkIOMeter_updateValues(Meter* this) { this->values[0] = cached_rxb_diff; this->values[1] = cached_txb_diff; - if (cached_rxb_diff + cached_txb_diff > this->total) { - this->total = cached_rxb_diff + cached_txb_diff; - } if (status == RATESTATUS_NODATA) { xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "no data"); @@ -171,7 +168,8 @@ const MeterClass NetworkIOMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 2, - .total = 100.0, + .isPercentChart = false, + .total = 1.0, .attributes = NetworkIOMeter_attributes, .name = "NetworkIO", .uiName = "Network IO", diff --git a/SwapMeter.c b/SwapMeter.c index 29c295d32..faa194556 100644 --- a/SwapMeter.c +++ b/SwapMeter.c @@ -75,6 +75,7 @@ const MeterClass SwapMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = SWAP_METER_ITEMCOUNT, + .isPercentChart = true, .total = 100.0, .attributes = SwapMeter_attributes, .name = "Swap", diff --git a/TasksMeter.c b/TasksMeter.c index fc1e4b0ed..f522b753b 100644 --- a/TasksMeter.c +++ b/TasksMeter.c @@ -34,7 +34,6 @@ static void TasksMeter_updateValues(Meter* this) { this->values[1] = pt->userlandThreads; this->values[2] = pt->totalTasks - pt->kernelThreads - pt->userlandThreads; this->values[3] = MINIMUM(pt->runningTasks, host->activeCPUs); - this->total = pt->totalTasks; xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%u/%u", MINIMUM(pt->runningTasks, host->activeCPUs), pt->totalTasks); } @@ -74,7 +73,8 @@ const MeterClass TasksMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 4, - .total = 100.0, + .isPercentChart = false, + .total = 1.0, .attributes = TasksMeter_attributes, .name = "Tasks", .uiName = "Task counter", diff --git a/XUtils.c b/XUtils.c index 94a14df64..248c572a6 100644 --- a/XUtils.c +++ b/XUtils.c @@ -12,6 +12,7 @@ in the source distribution for its full text. #include #include #include +#include // IWYU pragma: keep #include #include #include @@ -387,3 +388,14 @@ unsigned int countTrailingZeros(unsigned int x) { return mod37BitPosition[(-x & x) % 37]; } #endif + +#if !defined(HAVE_BUILTIN_CLZ) && !defined(HAVE_STDC_BIT_FLOOR) +/* Returns the nearest power of two that is not greater than x. + If x is 0, returns 0. */ +unsigned int powerOf2Floor(unsigned int x) { + for (unsigned int shift = 1; shift < sizeof(x) * CHAR_BIT; shift <<= 1) + x |= x >> shift; + + return x - (x >> 1); +} +#endif diff --git a/XUtils.h b/XUtils.h index 73335a611..f80d2d5a1 100644 --- a/XUtils.h +++ b/XUtils.h @@ -14,8 +14,10 @@ in the source distribution for its full text. #error "Must have #include \"config.h\" line at the top of the file that includes these XUtils helper functions" #endif +#include // IWYU pragma: keep #include #include // IWYU pragma: keep +#include // IWYU pragma: keep #include #include // IWYU pragma: keep #include // IWYU pragma: keep @@ -23,6 +25,16 @@ in the source distribution for its full text. #include "Compat.h" #include "Macros.h" +#ifdef HAVE_STDBIT_H +#include +#endif + +#if defined(HAVE_ARM_NEON_H) && defined(__ARM_NEON) +// ARM C Language Extensions (ACLE) recommends us to check __ARM_NEON before +// including +#include +#endif + ATTR_NORETURN void fail(void); @@ -149,6 +161,49 @@ static inline unsigned int countTrailingZeros(unsigned int x) { unsigned int countTrailingZeros(unsigned int x); #endif +/* Returns the nearest power of two that is not greater than x. + If x is 0, returns 0. */ +#if defined(HAVE_BUILTIN_CLZ) +static inline unsigned int powerOf2Floor(unsigned int x) { + if (x == 0) + return 0; + + return 1U << ((int)(sizeof(x) * CHAR_BIT) - 1 - __builtin_clz(x)); +} +#elif defined(HAVE_STDC_BIT_FLOOR) +static inline unsigned int powerOf2Floor(unsigned int x) { + return stdc_bit_floor_ui(x); +} +#else +unsigned int powerOf2Floor(unsigned int x); +#endif + +static inline unsigned int popCount8(uint8_t x) { +#if defined(HAVE_ARM_NEON_H) && defined(__ARM_NEON) + // With ARM Advanced SIMD extension (NEON), this generates smaller code than + // __builtin_popcount. + // + // Initialize the vector register. Set all lanes at once so that the + // compiler will not emit instruction to zero-initialize other lanes. + uint8x8_t v = vdup_n_u8(x); + // Count the number of set bits for each lane (8-bit) in the vector. + v = vcnt_u8(v); + // Get lane 0 and discard lanes 1 to 7. (Return type was uint8_t) + return vget_lane_u8(v, 0); +#elif defined(HAVE_BUILTIN_POPCOUNT) && defined(__POPCNT__) + // x86 POPCNT instruction. __builtin_popcount translates to it when it is + // enabled ("-mpopcnt"). (Return type was int) + return (unsigned int)__builtin_popcount(x); +#else + // This code is optimized for uint8_t input and smaller than the subroutine + // call of the compiler __builtin_popcount (which is tuned for + // unsigned int input type and not uint8_t). + uint32_t n = (uint32_t)(x * 0x08040201U); + n = (uint32_t)(((n >> 3) & 0x11111111U) * 0x11111111U) >> 28; + return n; +#endif +} + /* IEC unit prefixes */ static const char unitPrefixes[] = { 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q' }; diff --git a/configure.ac b/configure.ac index 3bbaa7159..3eac8974c 100644 --- a/configure.ac +++ b/configure.ac @@ -166,7 +166,10 @@ fi]) # Optional Section -AC_CHECK_HEADERS([execinfo.h]) +AC_CHECK_HEADERS([ \ + execinfo.h \ + stdbit.h \ + ]) if test "$my_htop_platform" = darwin; then AC_CHECK_HEADERS([mach/mach_time.h]) @@ -292,11 +295,47 @@ AC_LINK_IFELSE([ AC_MSG_CHECKING(for __builtin_ctz) AC_COMPILE_IFELSE([ - AC_LANG_PROGRAM([], [[__builtin_ctz(1); /* Supported in GCC 3.4 or later */]])], + AC_LANG_PROGRAM([], [[return __builtin_ctz(1U); /* Supported in GCC 3.4 or later */]])], [AC_DEFINE([HAVE_BUILTIN_CTZ], 1, [Define to 1 if the compiler supports '__builtin_ctz' function.]) AC_MSG_RESULT(yes)], AC_MSG_RESULT(no)) +AC_MSG_CHECKING(for __builtin_clz) +AC_COMPILE_IFELSE([ + AC_LANG_PROGRAM([], [[return __builtin_clz(-1U); /* Supported in GCC 3.4 or later */]])], + [AC_DEFINE([HAVE_BUILTIN_CLZ], 1, [Define to 1 if the compiler supports '__builtin_clz' function.]) + AC_MSG_RESULT(yes)], + AC_MSG_RESULT(no)) + +AC_MSG_CHECKING(for __builtin_popcount) +AC_COMPILE_IFELSE([ + AC_LANG_PROGRAM([], [[return __builtin_popcount(0U); /* Supported in GCC 3.4 or later */]])], + [AC_DEFINE([HAVE_BUILTIN_POPCOUNT], 1, [Define to 1 if the compiler supports '__builtin_popcount' function.]) + AC_MSG_RESULT(yes)], + AC_MSG_RESULT(no)) + +AC_MSG_CHECKING(for stdc_bit_floor) +AC_LINK_IFELSE([ + AC_LANG_PROGRAM( + [[ +#include + ]], + [[ + /* Both the type-generic and type-specific versions should exist. + htop uses the type-specific version. */ + return stdc_bit_floor(0U) || stdc_bit_floor_ui(0U); + ]])], + [AC_DEFINE([HAVE_STDC_BIT_FLOOR], 1, [Define to 1 if stdc_bit_floor functions are supported.]) + AC_MSG_RESULT(yes)], + AC_MSG_RESULT(no)) + +case "$host_cpu" in + arm*|aarch64*) + dnl ARM NEON intrinsics + AC_CHECK_HEADERS([arm_neon.h]) + ;; +esac + # ---------------------------------------------------------------------- diff --git a/linux/GPUMeter.c b/linux/GPUMeter.c index 628d4c71c..64ca5f9d3 100644 --- a/linux/GPUMeter.c +++ b/linux/GPUMeter.c @@ -172,6 +172,7 @@ const MeterClass GPUMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = ARRAYSIZE(GPUMeter_engineData) + 1, + .isPercentChart = true, .total = 100.0, .attributes = GPUMeter_attributes, .name = "GPU", diff --git a/linux/HugePageMeter.c b/linux/HugePageMeter.c index bd16f5dc2..f34bff8df 100644 --- a/linux/HugePageMeter.c +++ b/linux/HugePageMeter.c @@ -101,6 +101,7 @@ const MeterClass HugePageMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = ARRAYSIZE(HugePageMeter_active_labels), + .isPercentChart = true, .total = 100.0, .attributes = HugePageMeter_attributes, .name = "HugePages", diff --git a/linux/PressureStallMeter.c b/linux/PressureStallMeter.c index 5010c11d2..942213ea5 100644 --- a/linux/PressureStallMeter.c +++ b/linux/PressureStallMeter.c @@ -77,6 +77,7 @@ const MeterClass PressureStallCPUSomeMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallCPUSome", @@ -95,6 +96,7 @@ const MeterClass PressureStallIOSomeMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallIOSome", @@ -113,6 +115,7 @@ const MeterClass PressureStallIOFullMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallIOFull", @@ -131,6 +134,7 @@ const MeterClass PressureStallIRQFullMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallIRQFull", @@ -149,6 +153,7 @@ const MeterClass PressureStallMemorySomeMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallMemorySome", @@ -167,6 +172,7 @@ const MeterClass PressureStallMemoryFullMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallMemoryFull", diff --git a/linux/ZramMeter.c b/linux/ZramMeter.c index 2a1c7715c..fe10c3baa 100644 --- a/linux/ZramMeter.c +++ b/linux/ZramMeter.c @@ -77,6 +77,7 @@ const MeterClass ZramMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = ZRAM_METER_ITEMCOUNT, + .isPercentChart = true, .total = 100.0, .attributes = ZramMeter_attributes, .name = "Zram", diff --git a/zfs/ZfsArcMeter.c b/zfs/ZfsArcMeter.c index 87b7e19ce..8177ff2a5 100644 --- a/zfs/ZfsArcMeter.c +++ b/zfs/ZfsArcMeter.c @@ -93,6 +93,7 @@ const MeterClass ZfsArcMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 6, + .isPercentChart = true, .total = 100.0, .attributes = ZfsArcMeter_attributes, .name = "ZFSARC", diff --git a/zfs/ZfsCompressedArcMeter.c b/zfs/ZfsCompressedArcMeter.c index 35ab8b379..cd3bf43f6 100644 --- a/zfs/ZfsCompressedArcMeter.c +++ b/zfs/ZfsCompressedArcMeter.c @@ -81,6 +81,7 @@ const MeterClass ZfsCompressedArcMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 1, + .isPercentChart = true, .total = 100.0, .attributes = ZfsCompressedArcMeter_attributes, .name = "ZFSCARC",