Skip to content
This repository was archived by the owner on Aug 15, 2024. It is now read-only.

Commit a86c9d5

Browse files
committed
Allow to measure a single gameplay and specify gameplay duration
This allows us to calculate frame timeline statistics for the time when game is actually playing and fix the duration. The high-level goal is to improve experiment precision and reduce non-determinism from the raw data.
1 parent bbc3967 commit a86c9d5

File tree

1 file changed

+44
-18
lines changed

1 file changed

+44
-18
lines changed

scripts/analyze_frametimes.py

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class FrameTimeResult(object):
2929
Represents frametimes from a single frame time layer log.
3030
'''
3131
NanosPerSecond = 10**9
32+
NonosPerMilli = 10**6
3233
TargetFPS = 45
3334
TargetFrameTime = NanosPerSecond / TargetFPS
3435

@@ -43,7 +44,13 @@ def __init__(self):
4344
self.state_to_duration_ms = {}
4445

4546
@staticmethod
46-
def from_file(log_path, drop_first_seconds=None):
47+
def from_file(log_path, gameplay_state=None, gameplay_duration=None, drop_first_seconds=None):
48+
"""
49+
Creates a FrameTimeResult from a file.
50+
- if |gameplay_state| is specified, all frame time statistics will include frames only in that state;
51+
- if |gameplay_duration| is specified, all frame times after the specified duration (in seconds) will be discarded;
52+
- if |drop_first_seconds| is specified, the specified number of initial benchmark seconds will be discarded.
53+
"""
4754
full_path = path.realpath(log_path)
4855
base = path.basename(full_path)
4956
parent_dir = path.basename(path.dirname(full_path))
@@ -59,11 +66,21 @@ def from_file(log_path, drop_first_seconds=None):
5966
if i == 0:
6067
continue
6168
assert len(row) == 2
62-
result.raw_frametimes.append(int(row[0]))
63-
result.frame_states.append(int(row[1]))
64-
seen_states.add(int(row[1]))
6569

66-
if drop_first_seconds is not None and drop_first_seconds > 0:
70+
frametime_nanos = int(row[0])
71+
frame_state = int(row[1])
72+
seen_states.add(frame_state)
73+
if frame_state not in result.state_to_duration_ms:
74+
result.state_to_duration_ms[frame_state] = 0
75+
76+
result.state_to_duration_ms[frame_state] += frametime_nanos / result.NonosPerMilli
77+
if gameplay_state is not None and gameplay_state != frame_state:
78+
continue
79+
80+
result.raw_frametimes.append(frametime_nanos)
81+
result.frame_states.append(frame_state)
82+
83+
if drop_first_seconds is not None:
6784
drop_first_nanos = drop_first_seconds * result.NanosPerSecond
6885
curr_duration = 0
6986
drop_end = 0
@@ -77,18 +94,23 @@ def from_file(log_path, drop_first_seconds=None):
7794
result.raw_frametimes = result.raw_frametimes[drop_end:]
7895
result.frame_states = result.frame_states[drop_end:]
7996

80-
nanos_per_millis = 10**6
81-
result.total_duration_ms = np.sum(result.raw_frametimes) / nanos_per_millis
82-
result.average_frametime_ms = np.average(result.raw_frametimes) / nanos_per_millis
83-
result.percentile_frametime_ms = [np.percentile(result.raw_frametimes, p) / nanos_per_millis for p in range(100)]
97+
if gameplay_duration is not None:
98+
target_duration_nanos = gameplay_duration * result.NanosPerSecond
99+
curr_duration = 0
100+
first_frame_to_discard = 0
101+
for frametime in result.raw_frametimes:
102+
curr_duration += frametime
103+
first_frame_to_discard += 1
104+
if curr_duration > target_duration_nanos:
105+
break
84106

85-
for state in sorted(seen_states):
86-
nanos_in_state = 0
87-
for ft_ns, frame_state in zip(result.raw_frametimes, result.frame_states):
88-
if frame_state == state:
89-
nanos_in_state += ft_ns
107+
result.raw_frametimes = result.raw_frametimes[:first_frame_to_discard]
108+
result.frame_states = result.frame_states[:first_frame_to_discard]
90109

91-
result.state_to_duration_ms[state] = nanos_in_state / nanos_per_millis
110+
result.total_duration_ms = np.sum(result.raw_frametimes) / result.NonosPerMilli
111+
result.average_frametime_ms = np.average(result.raw_frametimes) / result.NonosPerMilli
112+
result.percentile_frametime_ms = \
113+
[np.percentile(result.raw_frametimes, p) / result.NonosPerMilli for p in range(100)]
92114

93115
return result
94116

@@ -206,13 +228,17 @@ def relative_noise(data):
206228
def main():
207229
parser = argparse.ArgumentParser(description='Process and analyze series of frame time logs.')
208230
parser.add_argument('--dataset', type=str, nargs='+', action='append', help='Dataset name followed by a list of log files: dataset_name log_file+')
209-
parser.add_argument('-c', '--cutoff', type=int, default=None, help='Number of seconds of the initial data to ignore')
231+
parser.add_argument('--duration', type=int, default=None, help='Number of seconds of gameplay data to analyze')
232+
parser.add_argument('--drop_front', type=int, default=None, help='Number of seconds of the initial data to ignore')
233+
parser.add_argument('--gameplay_state', type=int, default=None, help='The only state number to consider when calculating frame times statistics')
210234
parser.add_argument('--print_csv', type=bool, default=False, help='Prints stats as comma separated values (CSV)')
211235
parser.add_argument('-v', '--verbose', type=bool, default=False, help='Output gif file name')
212236
args = parser.parse_args()
213237

214238
datasets = args.dataset
215-
cutoff = args.cutoff
239+
gameplay_duration = args.duration
240+
drop_front = args.drop_front
241+
gameplay_state = args.gameplay_state
216242
use_csv = args.print_csv
217243
verbose = args.verbose
218244

@@ -221,7 +247,7 @@ def main():
221247
print(f'~~~~ Processing dataset {dataset_name} ~~~~')
222248
results = []
223249
for file in dataset[1:]:
224-
results.append(FrameTimeResult.from_file(file, cutoff))
250+
results.append(FrameTimeResult.from_file(file, gameplay_state, gameplay_duration, drop_front))
225251
if verbose:
226252
results[-1].dump()
227253
print()

0 commit comments

Comments
 (0)