Skip to content

Commit

Permalink
Added new configuration options for improved simulation time formatti…
Browse files Browse the repository at this point in the history
…ng in logs.
  • Loading branch information
LarsAsplund committed Aug 12, 2024
1 parent 84ac0f5 commit 25fea92
Show file tree
Hide file tree
Showing 9 changed files with 625 additions and 50 deletions.
167 changes: 165 additions & 2 deletions vunit/vhdl/logging/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,30 @@
from os import getenv
import glob
from pathlib import Path
from vunit import VUnit, location_preprocessor
from vunit import VUnit, VUnitCLI, location_preprocessor
from math import log10, floor, ceil
from random import seed, randint, random


def main():
vhdl_2019 = getenv("VUNIT_VHDL_STANDARD") == "2019"
root = Path(__file__).parent

ui = VUnit.from_argv()
cli = VUnitCLI()
cli.parser.add_argument(
"--performance-iterations",
type=int,
default=1,
help="Number of iterations to run in performance tests",
)
cli.parser.add_argument(
"--random-iterations",
type=int,
default=1,
help="Number of randomized tests",
)
args = cli.parse_args()
ui = VUnit.from_args(args=args)
ui.add_vhdl_builtins()

vunit_lib = ui.library("vunit_lib")
Expand All @@ -34,6 +50,153 @@ def main():

vunit_lib.test_bench("tb_location").set_generic("vhdl_2019", vhdl_2019)

tb = vunit_lib.test_bench("tb_sim_time_formatting")
tb.set_generic("n_performance_iterations", args.performance_iterations)

test_vectors = []
native_unit_scaling = 0
auto_unit_scaling = -1
full_resolution = -1

test_name = "Test native unit"
test_vectors.append([test_name, f"no_decimals", 123456, native_unit_scaling, 0])

test_name = "Test units of differents scales wrt the resolution_limit"
for scaling in [1, 1000, 1000000, 1000000000]:
test_vectors.append([test_name, f"scaling={scaling}", 123456, scaling, 0])

test_name = "Test auto unit"
test_vectors.append([test_name, "three_integer_digits", 456789, auto_unit_scaling, 0])
test_vectors.append([test_name, "two_integer_digits", 56789, auto_unit_scaling, 0])
test_vectors.append([test_name, "one_integer_digit", 6789, auto_unit_scaling, 0])

test_name = "Test limiting number of decimals"
for n_decimals in range(3):
for scaling in [1, 1000, 1000000]:
test_vectors.append([test_name, f"n_decimals={n_decimals}_scaling={scaling}", 456789, scaling, n_decimals])
test_vectors.append(
[test_name, f"n_decimals={n_decimals}_native_unit", 456789, native_unit_scaling, n_decimals]
)
test_vectors.append([test_name, f"n_decimals={n_decimals}_auto_unit", 456789, auto_unit_scaling, n_decimals])
test_vectors.append([test_name, f"no significant digits among decimals", 456789, 1000000000, 3])
test_vectors.append([test_name, f"some significant digits among decimals", 456789, 1000000000, 4])

test_name = "Test full resolution"
for scaling in [1, 1000, 1000000, 1000000000]:
test_vectors.append([test_name, f"scaling={scaling}", 123456, scaling, full_resolution])
test_vectors.append([test_name, f"native_unit", 123456, native_unit_scaling, full_resolution])
test_vectors.append([test_name, f"auto_unit", 123456, auto_unit_scaling, full_resolution])

test_name = "Test trailing zeros"
for n_zeros in range(-1, -7, -1):
test_time = round(123456, n_zeros)
test_vectors.append([test_name, f"n_zeros={-n_zeros}", test_time, 1000, 3])

test_name = "Test zero"
test_vectors.append([test_name, "auto_with_all_decimals", 0, auto_unit_scaling, full_resolution])
test_vectors.append([test_name, "scaling_with_fix_decimals", 0, 1000, 2])
test_vectors.append([test_name, "scaling_with_no_decimals", 0, 1000, 0])

def calculate_generics(test_time, scaling, n_decimals):
auto_scaling = (
1 if test_time == 0 else int(10 ** (floor(log10(test_time) / 3) * 3) if scaling == auto_unit_scaling else 1)
)
resolved_scaling = scaling if scaling > 0 else 1 if scaling == native_unit_scaling else auto_scaling

test_time_str = str(test_time)
n_decimals_to_use = int(log10(resolved_scaling))
if n_decimals_to_use >= len(test_time_str):
test_time_str = "0" * (n_decimals_to_use - len(test_time_str) + 1) + test_time_str

if (n_decimals_to_use == 0) and (n_decimals <= 0):
expected = test_time_str
elif (n_decimals_to_use == 0) and (n_decimals > 0):
expected = test_time_str + "." + "0" * n_decimals
elif n_decimals == 0:
expected = test_time_str[: len(test_time_str) - n_decimals_to_use]
elif (n_decimals < 0) or (n_decimals == n_decimals_to_use):
expected = (
test_time_str[: len(test_time_str) - n_decimals_to_use]
+ "."
+ test_time_str[len(test_time_str) - n_decimals_to_use :]
)
elif n_decimals < n_decimals_to_use:
expected = (
test_time_str[: len(test_time_str) - n_decimals_to_use]
+ "."
+ test_time_str[
len(test_time_str) - n_decimals_to_use : len(test_time_str) - n_decimals_to_use + n_decimals
]
)
else:
expected = (
test_time_str[: len(test_time_str) - n_decimals_to_use]
+ "."
+ test_time_str[len(test_time_str) - n_decimals_to_use :]
+ "0" * (n_decimals - n_decimals_to_use)
)

return auto_scaling, expected

for test_name, cfg_name, test_time, scaling, n_decimals in test_vectors:
test = tb.test(test_name)
auto_scaling, expected = calculate_generics(test_time, scaling, n_decimals)

test.add_config(
name=cfg_name,
generics=dict(
test_time=test_time,
scaling=scaling,
n_decimals=n_decimals,
expected=expected,
auto_scaling=auto_scaling,
),
)

test = tb.test("Test random")
seed("A seed")
cfg_names = set()
iter = 0
while iter < args.random_iterations:
rnd = randint(0, 24)
if rnd == 0:
test_time = 0
elif rnd == 1:
test_time = 1
elif rnd == 2:
test_time = 2**31 - 1
else:
test_time = randint(1, 10 ** randint(1, 9))

rnd = randint(0, 2)
if rnd == 0:
scaling = native_unit_scaling
elif rnd == 1:
scaling = auto_unit_scaling
else:
scaling = 10 ** (3 * randint(0, 3))

n_decimals = full_resolution if randint(0, 1) == 0 else randint(0, 15)

cfg_name = f"{test_time}_{scaling}_{n_decimals}"
if cfg_name in cfg_names:
continue

cfg_names.add(cfg_name)
iter += 1
auto_scaling, expected = calculate_generics(test_time, scaling, n_decimals)
test.add_config(
name=cfg_name,
generics=dict(
test_time=test_time,
scaling=scaling,
n_decimals=n_decimals,
expected=expected,
auto_scaling=auto_scaling,
),
)

ui.set_compile_option("rivierapro.vcom_flags", ["-dbg"])
ui.main()


Expand Down
160 changes: 159 additions & 1 deletion vunit/vhdl/logging/src/common_log_pkg-body.vhd
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,165 @@
--
-- Copyright (c) 2014-2024, Lars Asplund [email protected]

library ieee;
use ieee.math_real.all;

use std.textio.all;

package body common_log_pkg is
constant is_original_pkg : boolean := true;

function get_resolution_limit_as_log10_of_sec return integer is
constant t : string := time'image(time'high);
constant signature : character := t(t'length - 1);
begin
case signature is
when 'f' => return -15;
when 'p' => return -12;
when 'n' => return -9;
when 'u' => return -6;
when 'm' => return -3;
when others =>
report "Only resolution limits in the fs to ms range are supported. " &
"Only native simulation time formatting is supported" severity warning;
return -12;
end case;
end;

constant p_resolution_limit_as_log10_of_sec : integer := get_resolution_limit_as_log10_of_sec;

function p_to_string(
value : time;
unit : integer := p_native_unit;
n_decimals : integer := 0
) return string is
-- The typical simulator can handle time ranges wider than the integer range it can handle.
-- For example, 9876543210 fs i a valid time but 9876543210 is not a valid integer.
-- For that reason we cannot extract the integer part and then use normal arithmetic. Instead, the solution
-- is based on manipulating the string representation. This has two implications:
-- 1. When limiting the number of decimals, the value is truncated rather than rounded. However, when limiting
-- the number of decimals, the exact value is of no interest.
-- 2. Only units between fs and sec are supported. These are separated by powers of 10 and conversion
-- can be perfomed by moving the radix point. However, this approach of division is exact which a floating point
-- division may not be

constant value_str : string := time'image(value);

function unit_to_log10_of_sec(position_of_last_digit : positive) return integer is
begin
if unit = p_auto_unit then
return ((position_of_last_digit - 1) / 3) * 3 + p_resolution_limit_as_log10_of_sec;
elsif unit = p_native_unit then
return p_resolution_limit_as_log10_of_sec;
else
return unit;
end if;
end;

function max(a, b : integer) return integer is
begin
if a >= b then
return a;
end if;

return b;
end;

function min(a, b : integer) return integer is
begin
if a <= b then
return a;
end if;

return b;
end;

variable position_of_last_digit : positive;
variable unit_as_log10_of_sec : integer;
variable n_decimals_to_use : integer;
variable n_decimals_for_full_resolution : integer;
variable point_position : integer;
variable result : line;
variable position_of_last_decimal_to_use : integer;
variable position_of_first_decimal_to_use : integer;
variable n_added_decimals : natural;
variable n_decimals_to_add : integer;
begin
-- Shortcut for limiting performance impact on default behavior
if (unit = p_native_unit) and (n_decimals = 0) then
return value_str;
end if;

-- We assume that time is presented with a unit in the fs to ms range (space + 2 characters)
position_of_last_digit := value_str'length - 3;
unit_as_log10_of_sec := unit_to_log10_of_sec(position_of_last_digit);

-- Assuming that value_str is given in resolution_limit units
n_decimals_for_full_resolution := unit_as_log10_of_sec - p_resolution_limit_as_log10_of_sec;

-- digits before the point position are the integer part. The digits at the point position and after
-- are the decimal part
point_position := position_of_last_digit - n_decimals_for_full_resolution + 1;

if n_decimals = p_full_resolution then
n_decimals_to_use := n_decimals_for_full_resolution;
else
n_decimals_to_use := n_decimals;
end if;

-- Add integer part
if point_position > 1 then
write(result, value_str(1 to point_position - 1));
else
write(result, string'("0"));
end if;

-- Add decimal part
if (n_decimals_to_use > 0) then
write(result, string'("."));
n_added_decimals := 0;

-- Add leading zeros, for example 123 fs = 0.000123 ns
if -point_position + 1 > 0 then
n_added_decimals := min(n_decimals_to_use, -point_position + 1);
write(result, string'((1 to n_added_decimals => '0')));
end if;

-- Add digits from the value input
if n_added_decimals < n_decimals_to_use then
position_of_first_decimal_to_use := max(point_position, 1);
position_of_last_decimal_to_use := min(
position_of_first_decimal_to_use + n_decimals_to_use - n_added_decimals - 1,
position_of_last_digit
);
n_decimals_to_add := position_of_last_decimal_to_use - position_of_first_decimal_to_use + 1;
if n_decimals_to_add > 0 then
write(result, value_str(position_of_first_decimal_to_use to position_of_last_decimal_to_use));
n_added_decimals := n_added_decimals + n_decimals_to_add;
end if;
end if;

-- Add trailing zeros to get total number of decimals, for example 123 fs = 123.00 fs with 2 decimals
if n_added_decimals < n_decimals_to_use then
write(result, string'((1 to n_decimals_to_use - n_added_decimals => '0')));
end if;
end if;

-- Add unit
case unit_as_log10_of_sec is
when -15 => write(result, string'(" fs"));
when -12 => write(result, string'(" ps"));
when -9 => write(result, string'(" ns"));
when -6 => write(result, string'(" us"));
when -3 => write(result, string'(" ms"));
when 0 => write(result, string'(" sec"));
when others =>
report "Time unit not supported: " & integer'image(unit_as_log10_of_sec) severity failure;
end case;

return result.all;
end;

procedure write_to_log(
file log_destination : text;
log_destination_path : string := no_string;
Expand All @@ -19,6 +175,8 @@ package body common_log_pkg is
log_source_line_number : natural;
log_sequence_number : natural;
use_color : boolean;
log_time_unit : integer;
n_log_time_decimals : integer;
max_logger_name_length : natural
) is

Expand All @@ -38,7 +196,7 @@ package body common_log_pkg is
end;

procedure write_time(variable l : inout line; justify : boolean := false) is
constant time_string : string := time'image(log_time);
constant time_string : string := p_to_string(log_time, log_time_unit, n_log_time_decimals);
begin
if justify then
pad(l, max_time_length - time_string'length);
Expand Down
21 changes: 21 additions & 0 deletions vunit/vhdl/logging/src/common_log_pkg.vhd
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,28 @@ package common_log_pkg is
log_sequence_number : natural;
-- True if log entry is to be in color
use_color : boolean;
-- Unit to use for log time.
-- log_time_unit <= 0: unit = 10 ** log_time_unit.
-- log_time_unit = 1: native simulator unit.
-- log_time_unit = 2: unit such that log_time = n * unit where n is in the [0, 1000[ range.
log_time_unit : integer;
-- Number of decimals to use for log_time. If = -1 then the number of decimals is the number
-- yielding the full resolution provided by the simulator.
n_log_time_decimals : integer;
-- Max length of logger names (used for alignment)
max_logger_name_length : natural
);

-- This is not part of the public interface but exposes a private function for
-- testing purposes
constant p_resolution_limit_as_log10_of_sec : integer;
constant p_native_unit : integer := 1;
constant p_auto_unit : integer := 2;
constant p_full_resolution : integer := -1;
function p_to_string(
value : time;
unit : integer := p_native_unit;
n_decimals : integer := 0
) return string;

end package;
Loading

0 comments on commit 25fea92

Please sign in to comment.