// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert' show json;
import 'dart:math';

import 'package:file/file.dart';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_driver/src/driver/profiling_summarizer.dart';
import 'package:flutter_driver/src/driver/refresh_rate_summarizer.dart';
import 'package:flutter_driver/src/driver/scene_display_lag_summarizer.dart';
import 'package:flutter_driver/src/driver/vsync_frame_lag_summarizer.dart';
import 'package:path/path.dart' as path;

import '../../common.dart';

void main() {
  group('TimelineSummary', () {

    TimelineSummary summarize(List<Map<String, dynamic>> testEvents) {
      return TimelineSummary.summarize(Timeline.fromJson(<String, dynamic>{
        'traceEvents': testEvents,
      }));
    }

    Map<String, dynamic> frameBegin(int timeStamp) => <String, dynamic>{
      'name': 'Frame',
      'ph': 'B',
      'ts': timeStamp,
    };

    Map<String, dynamic> frameEnd(int timeStamp) => <String, dynamic>{
      'name': 'Frame',
      'ph': 'E',
      'ts': timeStamp,
    };

    Map<String, dynamic> begin(int timeStamp) => <String, dynamic>{
      'name': 'GPURasterizer::Draw',
      'ph': 'B',
      'ts': timeStamp,
    };

    Map<String, dynamic> end(int timeStamp) => <String, dynamic>{
      'name': 'GPURasterizer::Draw',
      'ph': 'E',
      'ts': timeStamp,
    };

    Map<String, dynamic> lagBegin(int timeStamp, int vsyncsMissed) => <String, dynamic>{
      'name': 'SceneDisplayLag',
      'ph': 'b',
      'ts': timeStamp,
      'args': <String, String>{
        'vsync_transitions_missed': vsyncsMissed.toString(),
      },
    };

    Map<String, dynamic> lagEnd(int timeStamp, int vsyncsMissed) => <String, dynamic>{
      'name': 'SceneDisplayLag',
      'ph': 'e',
      'ts': timeStamp,
      'args': <String, String>{
        'vsync_transitions_missed': vsyncsMissed.toString(),
      },
    };

    Map<String, dynamic> cpuUsage(int timeStamp, double cpuUsage) => <String, dynamic>{
      'cat': 'embedder',
      'name': 'CpuUsage',
      'ts': timeStamp,
      'args': <String, String>{
        'total_cpu_usage': cpuUsage.toString(),
      },
    };

    Map<String, dynamic> memoryUsage(int timeStamp, double dirty, double shared) => <String, dynamic>{
      'cat': 'embedder',
      'name': 'MemoryUsage',
      'ts': timeStamp,
      'args': <String, String>{
        'owned_shared_memory_usage': shared.toString(),
        'dirty_memory_usage': dirty.toString(),
      },
    };

    Map<String, dynamic> platformVsync(int timeStamp) => <String, dynamic>{
      'name': 'VSYNC',
      'ph': 'B',
      'ts': timeStamp,
    };

    Map<String, dynamic> vsyncCallback(int timeStamp, {String phase = 'B', String startTime = '2750850055428', String endTime = '2750866722095'}) => <String, dynamic>{
      'name': 'VsyncProcessCallback',
      'ph': phase,
      'ts': timeStamp,
      'args': <String, dynamic>{
        'StartTime': startTime,
        'TargetTime': endTime,
      },
    };

    List<Map<String, dynamic>> genGC(String name, int count, int startTime, int timeDiff) {
      int ts = startTime;
      bool begin = true;
      final List<Map<String, dynamic>> ret = <Map<String, dynamic>>[];
      for (int i = 0; i < count; i++) {
        ret.add(<String, dynamic>{
          'name': name,
          'cat': 'GC',
          'tid': 19695,
          'pid': 19650,
          'ts': ts,
          'tts': ts,
          'ph': begin ? 'B' : 'E',
          'args': <String, dynamic>{
            'isolateGroupId': 'isolateGroups/10824099774666259225',
          },
        });
        ts = ts + timeDiff;
        begin = !begin;
      }
      return ret;
    }

    List<Map<String, dynamic>> newGenGC(int count, int startTime, int timeDiff) {
      return genGC('CollectNewGeneration', count, startTime, timeDiff);
    }

    List<Map<String, dynamic>> oldGenGC(int count, int startTime, int timeDiff) {
      return genGC('CollectOldGeneration', count, startTime, timeDiff);
    }

    List<Map<String, dynamic>> rasterizeTimeSequenceInMillis(List<int> sequence) {
      final List<Map<String, dynamic>> result = <Map<String, dynamic>>[];
      int t = 0;
      for (final int duration in sequence) {
        result.add(begin(t));
        t += duration * 1000;
        result.add(end(t));
      }
      return result;
    }

    group('frame_count', () {
      test('counts frames', () {
        expect(
          summarize(<Map<String, dynamic>>[
            frameBegin(1000), frameEnd(2000),
            frameBegin(3000), frameEnd(5000),
          ]).countFrames(),
          2,
        );
      });
    });

    group('average_frame_build_time_millis', () {
      test('throws when there is no data', () {
        expect(
          () => summarize(<Map<String, dynamic>>[]).computeAverageFrameBuildTimeMillis(),
          throwsA(
            isA<StateError>()
              .having((StateError e) => e.message,
              'message',
              contains('The TimelineSummary had no events to summarize.'),
            )),
        );
      });

      test('computes average frame build time in milliseconds', () {
        expect(
          summarize(<Map<String, dynamic>>[
            frameBegin(1000), frameEnd(2000),
            frameBegin(3000), frameEnd(5000),
          ]).computeAverageFrameBuildTimeMillis(),
          1.5,
        );
      });

      test('skips leading "end" events', () {
        expect(
          summarize(<Map<String, dynamic>>[
            frameEnd(1000),
            frameBegin(2000), frameEnd(4000),
          ]).computeAverageFrameBuildTimeMillis(),
          2.0,
        );
      });

      test('skips trailing "begin" events', () {
        expect(
          summarize(<Map<String, dynamic>>[
            frameBegin(2000), frameEnd(4000),
            frameBegin(5000),
          ]).computeAverageFrameBuildTimeMillis(),
          2.0,
        );
      });

      // see https://github.com/flutter/flutter/issues/54095.
      test('ignore multiple "end" events', () {
        expect(
          summarize(<Map<String, dynamic>>[
            frameBegin(2000), frameEnd(4000),
            frameEnd(4300), // rogue frame end.
            frameBegin(5000), frameEnd(6000),
          ]).computeAverageFrameBuildTimeMillis(),
          1.5,
        );
      });

      test('pick latest when there are multiple "begin" events', () {
        expect(
          summarize(<Map<String, dynamic>>[
            frameBegin(1000), // rogue frame begin.
            frameBegin(2000), frameEnd(4000),
            frameEnd(4300), // rogue frame end.
            frameBegin(4400), // rogue frame begin.
            frameBegin(5000), frameEnd(6000),
          ]).computeAverageFrameBuildTimeMillis(),
          1.5,
        );
      });
    });

    group('worst_frame_build_time_millis', () {
      test('throws when there is no data', () {
        expect(
          () => summarize(<Map<String, dynamic>>[]).computeWorstFrameBuildTimeMillis(),
          throwsA(
            isA<StateError>()
              .having((StateError e) => e.message,
              'message',
              contains('The TimelineSummary had no events to summarize.'),
            )),
        );
      });

      test('computes worst frame build time in milliseconds', () {
        expect(
          summarize(<Map<String, dynamic>>[
            frameBegin(1000), frameEnd(2000),
            frameBegin(3000), frameEnd(5000),
          ]).computeWorstFrameBuildTimeMillis(),
          2.0,
        );
        expect(
          summarize(<Map<String, dynamic>>[
            frameBegin(3000), frameEnd(5000),
            frameBegin(1000), frameEnd(2000),
          ]).computeWorstFrameBuildTimeMillis(),
          2.0,
        );
      });

      test('skips leading "end" events', () {
        expect(
          summarize(<Map<String, dynamic>>[
            frameEnd(1000),
            frameBegin(2000), frameEnd(4000),
          ]).computeWorstFrameBuildTimeMillis(),
          2.0,
        );
      });

      test('skips trailing "begin" events', () {
        expect(
          summarize(<Map<String, dynamic>>[
            frameBegin(2000), frameEnd(4000),
            frameBegin(5000),
          ]).computeWorstFrameBuildTimeMillis(),
          2.0,
        );
      });
    });

    group('computeMissedFrameBuildBudgetCount', () {
      test('computes the number of missed build budgets', () {
        final TimelineSummary summary = summarize(<Map<String, dynamic>>[
          frameBegin(1000), frameEnd(18000),
          frameBegin(19000), frameEnd(28000),
          frameBegin(29000), frameEnd(47000),
        ]);

        expect(summary.countFrames(), 3);
        expect(summary.computeMissedFrameBuildBudgetCount(), 2);
      });
    });

    group('average_frame_rasterizer_time_millis', () {
      test('throws when there is no data', () {
        expect(
          () => summarize(<Map<String, dynamic>>[]).computeAverageFrameRasterizerTimeMillis(),
          throwsA(
            isA<StateError>()
              .having((StateError e) => e.message,
              'message',
              contains('The TimelineSummary had no events to summarize.'),
            )),
        );
      });

      test('computes average frame rasterizer time in milliseconds', () {
        expect(
            summarize(<Map<String, dynamic>>[
              begin(1000), end(2000),
              begin(3000), end(5000),
            ]).computeAverageFrameRasterizerTimeMillis(),
            1.5,
        );
      });

      test('skips leading "end" events', () {
        expect(
            summarize(<Map<String, dynamic>>[
              end(1000),
              begin(2000), end(4000),
            ]).computeAverageFrameRasterizerTimeMillis(),
            2.0,
        );
      });

      test('skips trailing "begin" events', () {
        expect(
            summarize(<Map<String, dynamic>>[
              begin(2000), end(4000),
              begin(5000),
            ]).computeAverageFrameRasterizerTimeMillis(),
            2.0,
        );
      });
    });

    group('worst_frame_rasterizer_time_millis', () {
      test('throws when there is no data', () {
        expect(
          () => summarize(<Map<String, dynamic>>[]).computeWorstFrameRasterizerTimeMillis(),
          throwsA(
            isA<StateError>()
              .having((StateError e) => e.message,
              'message',
              contains('The TimelineSummary had no events to summarize.'),
            )),
        );
      });


      test('computes worst frame rasterizer time in milliseconds', () {
        expect(
            summarize(<Map<String, dynamic>>[
              begin(1000), end(2000),
              begin(3000), end(5000),
            ]).computeWorstFrameRasterizerTimeMillis(),
            2.0,
        );
        expect(
            summarize(<Map<String, dynamic>>[
              begin(3000), end(5000),
              begin(1000), end(2000),
            ]).computeWorstFrameRasterizerTimeMillis(),
            2.0,
        );
      });

      test('skips leading "end" events', () {
        expect(
            summarize(<Map<String, dynamic>>[
              end(1000),
              begin(2000), end(4000),
            ]).computeWorstFrameRasterizerTimeMillis(),
            2.0,
        );
      });

      test('skips trailing "begin" events', () {
        expect(
            summarize(<Map<String, dynamic>>[
              begin(2000), end(4000),
              begin(5000),
            ]).computeWorstFrameRasterizerTimeMillis(),
            2.0,
        );
      });
    });

    group('percentile_frame_rasterizer_time_millis', () {
      test('throws when there is no data', () {
        expect(
          () => summarize(<Map<String, dynamic>>[]).computePercentileFrameRasterizerTimeMillis(90.0),
          throwsA(
            isA<StateError>()
              .having((StateError e) => e.message,
              'message',
              contains('The TimelineSummary had no events to summarize.'),
            )),
        );
      });


      const List<List<int>> sequences = <List<int>>[
        <int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
        <int>[1, 2, 3, 4, 5],
        <int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
      ];

      const List<int> p90s = <int>[
        9,
        5,
        18,
      ];

      test('computes 90th frame rasterizer time in milliseconds', () {
        for (int i = 0; i < sequences.length; ++i) {
          expect(
            summarize(rasterizeTimeSequenceInMillis(sequences[i])).computePercentileFrameRasterizerTimeMillis(90.0),
            p90s[i],
          );
        }
      });

      test('compute 99th frame rasterizer time in milliseconds', () {
        final List<int> sequence = <int>[];
        for (int i = 1; i <= 100; ++i) {
          sequence.add(i);
        }
        expect(
          summarize(rasterizeTimeSequenceInMillis(sequence)).computePercentileFrameRasterizerTimeMillis(99.0),
          99,
        );
      });
    });

    group('computeMissedFrameRasterizerBudgetCount', () {
      test('computes the number of missed rasterizer budgets', () {
        final TimelineSummary summary = summarize(<Map<String, dynamic>>[
          begin(1000), end(18000),
          begin(19000), end(28000),
          begin(29000), end(47000),
        ]);

        expect(summary.computeMissedFrameRasterizerBudgetCount(), 2);
      });
    });

    group('summaryJson', () {
      test('computes and returns summary as JSON', () {
        expect(
          summarize(<Map<String, dynamic>>[
            begin(1000), end(19000),
            begin(19000), end(29000),
            begin(29000), end(49000),
            ...newGenGC(4, 10, 100),
            ...oldGenGC(5, 10000, 100),
            frameBegin(1000), frameEnd(18000),
            frameBegin(19000), frameEnd(28000),
            frameBegin(29000), frameEnd(48000),
          ]).summaryJson,
          <String, dynamic>{
            'average_frame_build_time_millis': 15.0,
            '90th_percentile_frame_build_time_millis': 19.0,
            '99th_percentile_frame_build_time_millis': 19.0,
            'worst_frame_build_time_millis': 19.0,
            'missed_frame_build_budget_count': 2,
            'average_frame_rasterizer_time_millis': 16.0,
            '90th_percentile_frame_rasterizer_time_millis': 20.0,
            '99th_percentile_frame_rasterizer_time_millis': 20.0,
            'worst_frame_rasterizer_time_millis': 20.0,
            'missed_frame_rasterizer_budget_count': 2,
            'frame_count': 3,
            'frame_rasterizer_count': 3,
            'new_gen_gc_count': 4,
            'old_gen_gc_count': 5,
            'frame_build_times': <int>[17000, 9000, 19000],
            'frame_rasterizer_times': <int>[18000, 10000, 20000],
            'frame_begin_times': <int>[0, 18000, 28000],
            'frame_rasterizer_begin_times': <int>[0, 18000, 28000],
            'average_vsync_transitions_missed': 0.0,
            '90th_percentile_vsync_transitions_missed': 0.0,
            '99th_percentile_vsync_transitions_missed': 0.0,
            'average_vsync_frame_lag': 0.0,
            '90th_percentile_vsync_frame_lag': 0.0,
            '99th_percentile_vsync_frame_lag': 0.0,
            'average_layer_cache_count': 0.0,
            '90th_percentile_layer_cache_count': 0.0,
            '99th_percentile_layer_cache_count': 0.0,
            'worst_layer_cache_count': 0.0,
            'average_layer_cache_memory': 0.0,
            '90th_percentile_layer_cache_memory': 0.0,
            '99th_percentile_layer_cache_memory': 0.0,
            'worst_layer_cache_memory': 0.0,
            'average_picture_cache_count': 0.0,
            '90th_percentile_picture_cache_count': 0.0,
            '99th_percentile_picture_cache_count': 0.0,
            'worst_picture_cache_count': 0.0,
            'average_picture_cache_memory': 0.0,
            '90th_percentile_picture_cache_memory': 0.0,
            '99th_percentile_picture_cache_memory': 0.0,
            'worst_picture_cache_memory': 0.0,
            'total_ui_gc_time': 0.4,
            '30hz_frame_percentage': 0,
            '60hz_frame_percentage': 0,
            '80hz_frame_percentage': 0,
            '90hz_frame_percentage': 0,
            '120hz_frame_percentage': 0,
            'illegal_refresh_rate_frame_count': 0,
          },
        );
      });
    });

    group('writeTimelineToFile', () {

      late Directory tempDir;

      setUp(() {
        useMemoryFileSystemForTesting();
        tempDir = fs.systemTempDirectory.createTempSync('flutter_driver_test.');
      });

      tearDown(() {
        tryToDelete(tempDir);
        restoreFileSystem();
      });

      test('writes timeline to JSON file without summary', () async {
        await summarize(<Map<String, String>>[<String, String>{'foo': 'bar'}])
          .writeTimelineToFile('test', destinationDirectory: tempDir.path, includeSummary: false);
        final String written =
            await fs.file(path.join(tempDir.path, 'test.timeline.json')).readAsString();
        expect(written, '{"traceEvents":[{"foo":"bar"}]}');
      });

      test('writes timeline to JSON file with summary', () async {
        await summarize(<Map<String, dynamic>>[
          <String, String>{'foo': 'bar'},
          begin(1000), end(19000),
          frameBegin(1000), frameEnd(18000),
        ]).writeTimelineToFile(
          'test',
          destinationDirectory: tempDir.path,
        );
        final String written =
            await fs.file(path.join(tempDir.path, 'test.timeline.json')).readAsString();
        expect(
          written,
          '{"traceEvents":[{"foo":"bar"},'
          '{"name":"GPURasterizer::Draw","ph":"B","ts":1000},'
          '{"name":"GPURasterizer::Draw","ph":"E","ts":19000},'
          '{"name":"Frame","ph":"B","ts":1000},'
          '{"name":"Frame","ph":"E","ts":18000}]}',
        );
      });

      test('writes summary to JSON file', () async {
        await summarize(<Map<String, dynamic>>[
          begin(1000), end(19000),
          begin(19000), end(29000),
          begin(29000), end(49000),
          frameBegin(1000), frameEnd(18000),
          frameBegin(19000), frameEnd(28000),
          frameBegin(29000), frameEnd(48000),
          lagBegin(1000, 4), lagEnd(2000, 4),
          lagBegin(1200, 12), lagEnd(2400, 12),
          lagBegin(4200, 8), lagEnd(9400, 8),
          ...newGenGC(4, 10, 100),
          ...oldGenGC(5, 10000, 100),
          cpuUsage(5000, 20), cpuUsage(5010, 60),
          memoryUsage(6000, 20, 40), memoryUsage(6100, 30, 45),
          platformVsync(7000), vsyncCallback(7500),
        ]).writeTimelineToFile('test', destinationDirectory: tempDir.path);
        final String written =
            await fs.file(path.join(tempDir.path, 'test.timeline_summary.json')).readAsString();
        expect(json.decode(written), <String, dynamic>{
          'average_frame_build_time_millis': 15.0,
          'worst_frame_build_time_millis': 19.0,
          '90th_percentile_frame_build_time_millis': 19.0,
          '99th_percentile_frame_build_time_millis': 19.0,
          'missed_frame_build_budget_count': 2,
          'average_frame_rasterizer_time_millis': 16.0,
          '90th_percentile_frame_rasterizer_time_millis': 20.0,
          '99th_percentile_frame_rasterizer_time_millis': 20.0,
          'worst_frame_rasterizer_time_millis': 20.0,
          'missed_frame_rasterizer_budget_count': 2,
          'frame_count': 3,
          'frame_rasterizer_count': 3,
          'new_gen_gc_count': 4,
          'old_gen_gc_count': 5,
          'frame_build_times': <int>[17000, 9000, 19000],
          'frame_rasterizer_times': <int>[18000, 10000, 20000],
          'frame_begin_times': <int>[0, 18000, 28000],
          'frame_rasterizer_begin_times': <int>[0, 18000, 28000],
          'average_vsync_transitions_missed': 8.0,
          '90th_percentile_vsync_transitions_missed': 12.0,
          '99th_percentile_vsync_transitions_missed': 12.0,
          'average_vsync_frame_lag': 500.0,
          '90th_percentile_vsync_frame_lag': 500.0,
          '99th_percentile_vsync_frame_lag': 500.0,
          'average_cpu_usage': 40.0,
          '90th_percentile_cpu_usage': 60.0,
          '99th_percentile_cpu_usage': 60.0,
          'average_memory_usage': 67.5,
          '90th_percentile_memory_usage': 75.0,
          '99th_percentile_memory_usage': 75.0,
          'average_layer_cache_count': 0.0,
          '90th_percentile_layer_cache_count': 0.0,
          '99th_percentile_layer_cache_count': 0.0,
          'worst_layer_cache_count': 0.0,
          'average_layer_cache_memory': 0.0,
          '90th_percentile_layer_cache_memory': 0.0,
          '99th_percentile_layer_cache_memory': 0.0,
          'worst_layer_cache_memory': 0.0,
          'average_picture_cache_count': 0.0,
          '90th_percentile_picture_cache_count': 0.0,
          '99th_percentile_picture_cache_count': 0.0,
          'worst_picture_cache_count': 0.0,
          'average_picture_cache_memory': 0.0,
          '90th_percentile_picture_cache_memory': 0.0,
          '99th_percentile_picture_cache_memory': 0.0,
          'worst_picture_cache_memory': 0.0,
          'total_ui_gc_time': 0.4,
          '30hz_frame_percentage': 0,
          '60hz_frame_percentage': 100,
          '80hz_frame_percentage': 0,
          '90hz_frame_percentage': 0,
          '120hz_frame_percentage': 0,
          'illegal_refresh_rate_frame_count': 0,
        });
      });
    });

    group('SceneDisplayLagSummarizer tests', () {
      SceneDisplayLagSummarizer summarize(List<Map<String, dynamic>> traceEvents) {
          final Timeline timeline = Timeline.fromJson(<String, dynamic>{
          'traceEvents': traceEvents,
          });
          return SceneDisplayLagSummarizer(timeline.events!);
      }

      test('average_vsyncs_missed', () async {
        final SceneDisplayLagSummarizer summarizer = summarize(<Map<String, dynamic>>[
          lagBegin(1000, 4), lagEnd(2000, 4),
          lagBegin(1200, 12), lagEnd(2400, 12),
          lagBegin(4200, 8), lagEnd(9400, 8),
        ]);
        expect(summarizer.computeAverageVsyncTransitionsMissed(), 8.0);
      });

      test('all stats are 0 for 0 missed transitions', () async {
        final SceneDisplayLagSummarizer summarizer = summarize(<Map<String, dynamic>>[]);
        expect(summarizer.computeAverageVsyncTransitionsMissed(), 0.0);
        expect(summarizer.computePercentileVsyncTransitionsMissed(90.0), 0.0);
        expect(summarizer.computePercentileVsyncTransitionsMissed(99.0), 0.0);
      });

      test('90th_percentile_vsyncs_missed', () async {
        final SceneDisplayLagSummarizer summarizer = summarize(<Map<String, dynamic>>[
          lagBegin(1000, 4), lagEnd(2000, 4),
          lagBegin(1200, 12), lagEnd(2400, 12),
          lagBegin(4200, 8), lagEnd(9400, 8),
          lagBegin(6100, 14), lagEnd(11000, 14),
          lagBegin(7100, 16), lagEnd(11500, 16),
          lagBegin(7400, 11), lagEnd(13000, 11),
          lagBegin(8200, 27), lagEnd(14100, 27),
          lagBegin(8700, 7), lagEnd(14300, 7),
          lagBegin(24200, 4187), lagEnd(39400, 4187),
        ]);
        expect(summarizer.computePercentileVsyncTransitionsMissed(90), 27.0);
      });

      test('99th_percentile_vsyncs_missed', () async {
        final SceneDisplayLagSummarizer summarizer = summarize(<Map<String, dynamic>>[
          lagBegin(1000, 4), lagEnd(2000, 4),
          lagBegin(1200, 12), lagEnd(2400, 12),
          lagBegin(4200, 8), lagEnd(9400, 8),
          lagBegin(6100, 14), lagEnd(11000, 14),
          lagBegin(24200, 4187), lagEnd(39400, 4187),
        ]);
        expect(summarizer.computePercentileVsyncTransitionsMissed(99), 4187.0);
      });
    });

    group('ProfilingSummarizer tests', () {
      ProfilingSummarizer summarize(List<Map<String, dynamic>> traceEvents) {
          final Timeline timeline = Timeline.fromJson(<String, dynamic>{
            'traceEvents': traceEvents,
          });
          return ProfilingSummarizer.fromEvents(timeline.events!);
      }

      test('has_both_cpu_and_memory_usage', () async {
        final ProfilingSummarizer summarizer = summarize(<Map<String, dynamic>>[
          cpuUsage(0, 10),
          memoryUsage(0, 6, 10),
          cpuUsage(0, 12),
          memoryUsage(0, 8, 40),
        ]);
        expect(summarizer.computeAverage(ProfileType.CPU), 11.0);
        expect(summarizer.computeAverage(ProfileType.Memory), 32.0);
      });

      test('has_only_memory_usage', () async {
        final ProfilingSummarizer summarizer = summarize(<Map<String, dynamic>>[
          memoryUsage(0, 6, 10),
          memoryUsage(0, 8, 40),
        ]);
        expect(summarizer.computeAverage(ProfileType.Memory), 32.0);
        expect(summarizer.summarize().containsKey('average_cpu_usage'), false);
      });

      test('90th_percentile_cpu_usage', () async {
        final ProfilingSummarizer summarizer = summarize(<Map<String, dynamic>>[
          cpuUsage(0, 10), cpuUsage(1, 20),
          cpuUsage(2, 20), cpuUsage(3, 80),
          cpuUsage(4, 70), cpuUsage(4, 72),
          cpuUsage(4, 85), cpuUsage(4, 100),
        ]);
        expect(summarizer.computePercentile(ProfileType.CPU, 90), 85.0);
      });
    });

    group('VsyncFrameLagSummarizer tests', () {
      VsyncFrameLagSummarizer summarize(List<Map<String, dynamic>> traceEvents) {
        final Timeline timeline = Timeline.fromJson(<String, dynamic>{
          'traceEvents': traceEvents,
        });
        return VsyncFrameLagSummarizer(timeline.events!);
      }

      test('average_vsync_frame_lag', () async {
        final VsyncFrameLagSummarizer summarizer = summarize(<Map<String, dynamic>>[
          platformVsync(10),
          vsyncCallback(12),
          platformVsync(16),
          vsyncCallback(29),
        ]);
        expect(summarizer.computeAverageVsyncFrameLag(), 7.5);
      });

      test('malformed_event_ordering', () async {
        final VsyncFrameLagSummarizer summarizer = summarize(<Map<String, dynamic>>[
          vsyncCallback(10),
          platformVsync(10),
        ]);
        expect(summarizer.computeAverageVsyncFrameLag(), 0);
        expect(summarizer.computePercentileVsyncFrameLag(80), 0);
      });

      test('penalize_consecutive_vsyncs', () async {
        final VsyncFrameLagSummarizer summarizer = summarize(<Map<String, dynamic>>[
          platformVsync(10),
          platformVsync(12),
        ]);
        expect(summarizer.computeAverageVsyncFrameLag(), 2);
      });

      test('pick_nearest_platform_vsync', () async {
        final VsyncFrameLagSummarizer summarizer = summarize(<Map<String, dynamic>>[
          platformVsync(10),
          platformVsync(12),
          vsyncCallback(18),
        ]);
        expect(summarizer.computeAverageVsyncFrameLag(), 4);
      });

      test('percentile_vsync_frame_lag', () async {
        final List<Map<String, dynamic>> events = <Map<String, dynamic>>[];
        int ts = 100;
        for (int i = 0; i < 100; i++) {
          events.add(platformVsync(ts));
          ts = ts + 10 * (i + 1);
          events.add(vsyncCallback(ts));
        }

        final VsyncFrameLagSummarizer summarizer = summarize(events);
        expect(summarizer.computePercentileVsyncFrameLag(90), 890);
        expect(summarizer.computePercentileVsyncFrameLag(99), 990);
      });
    });

    group('RefreshRateSummarizer tests', () {

      const double kCompareDelta = 0.01;
      RefreshRateSummary summarizeRefresh(List<Map<String, dynamic>> traceEvents) {
        final Timeline timeline = Timeline.fromJson(<String, dynamic>{
          'traceEvents': traceEvents,
        });
        return RefreshRateSummary(vsyncEvents: timeline.events!);
      }

      List<Map<String, dynamic>> populateEvents({required int numberOfEvents, required  int startTime, required int interval, required int margin}) {
        final List<Map<String, dynamic>> events = <Map<String, dynamic>>[];
        int startTimeInNanoseconds = startTime;
        for (int i = 0; i < numberOfEvents; i ++) {
          final int randomMargin = margin >= 1 ? (-margin + Random().nextInt(margin*2)) : 0;
          final int endTime = startTimeInNanoseconds + interval + randomMargin;
          events.add(vsyncCallback(0, startTime: startTimeInNanoseconds.toString(), endTime: endTime.toString()));
          startTimeInNanoseconds = endTime;
        }
        return events;
      }

      test('Recognize 30 hz frames.', () async {
        const int startTimeInNanoseconds = 2750850055430;
        const int intervalInNanoseconds = 33333333;
        // allow some margins
        const int margin = 3000000;
        final List<Map<String, dynamic>> events = populateEvents(numberOfEvents: 100,
                                                                  startTime: startTimeInNanoseconds,
                                                                  interval: intervalInNanoseconds,
                                                                  margin: margin,
                                                                 );
        final RefreshRateSummary summary = summarizeRefresh(events);
        expect(summary.percentageOf30HzFrames, closeTo(100, kCompareDelta));
        expect(summary.percentageOf60HzFrames, 0);
        expect(summary.percentageOf90HzFrames, 0);
        expect(summary.percentageOf120HzFrames, 0);
        expect(summary.framesWithIllegalRefreshRate, isEmpty);
      });

      test('Recognize 60 hz frames.', () async {
        const int startTimeInNanoseconds = 2750850055430;
        const int intervalInNanoseconds = 16666666;
        // allow some margins
        const int margin = 1200000;
        final List<Map<String, dynamic>> events = populateEvents(numberOfEvents: 100,
                                                                  startTime: startTimeInNanoseconds,
                                                                  interval: intervalInNanoseconds,
                                                                  margin: margin,
                                                                 );

        final RefreshRateSummary summary = summarizeRefresh(events);
        expect(summary.percentageOf30HzFrames, 0);
        expect(summary.percentageOf60HzFrames, closeTo(100, kCompareDelta));
        expect(summary.percentageOf90HzFrames, 0);
        expect(summary.percentageOf120HzFrames, 0);
        expect(summary.framesWithIllegalRefreshRate, isEmpty);
      });

      test('Recognize 90 hz frames.', () async {
        const int startTimeInNanoseconds = 2750850055430;
        const int intervalInNanoseconds = 11111111;
        // allow some margins
        const int margin = 500000;
        final List<Map<String, dynamic>> events = populateEvents(numberOfEvents: 100,
                                                                  startTime: startTimeInNanoseconds,
                                                                  interval: intervalInNanoseconds,
                                                                  margin: margin,
                                                                 );

        final RefreshRateSummary summary = summarizeRefresh(events);
        expect(summary.percentageOf30HzFrames, 0);
        expect(summary.percentageOf60HzFrames, 0);
        expect(summary.percentageOf90HzFrames, closeTo(100, kCompareDelta));
        expect(summary.percentageOf120HzFrames, 0);
        expect(summary.framesWithIllegalRefreshRate, isEmpty);
      });

      test('Recognize 120 hz frames.', () async {
        const int startTimeInNanoseconds = 2750850055430;
        const int intervalInNanoseconds = 8333333;
        // allow some margins
        const int margin = 300000;
        final List<Map<String, dynamic>> events = populateEvents(numberOfEvents: 100,
                                                                  startTime: startTimeInNanoseconds,
                                                                  interval: intervalInNanoseconds,
                                                                  margin: margin,
                                                                 );
        final RefreshRateSummary summary = summarizeRefresh(events);
        expect(summary.percentageOf30HzFrames, 0);
        expect(summary.percentageOf60HzFrames, 0);
        expect(summary.percentageOf90HzFrames, 0);
        expect(summary.percentageOf120HzFrames, closeTo(100, kCompareDelta));
        expect(summary.framesWithIllegalRefreshRate, isEmpty);
      });

      test('Identify illegal refresh rates.', () async {
        const int startTimeInNanoseconds = 2750850055430;
        const int intervalInNanoseconds = 10000000;
        final List<Map<String, dynamic>> events = populateEvents(numberOfEvents: 1,
                                                                  startTime: startTimeInNanoseconds,
                                                                  interval: intervalInNanoseconds,
                                                                  margin: 0,
                                                                 );
        final RefreshRateSummary summary = summarizeRefresh(events);
        expect(summary.percentageOf30HzFrames, 0);
        expect(summary.percentageOf60HzFrames, 0);
        expect(summary.percentageOf90HzFrames, 0);
        expect(summary.percentageOf120HzFrames, 0);
        expect(summary.framesWithIllegalRefreshRate, isNotEmpty);
        expect(summary.framesWithIllegalRefreshRate.first, closeTo(100, kCompareDelta));
      });

      test('Mixed refresh rates.', () async {

        final List<Map<String, dynamic>> events = <Map<String, dynamic>>[];
        const int num30Hz = 10;
        const int num60Hz = 20;
        const int num80Hz = 20;
        const int num90Hz = 20;
        const int num120Hz = 40;
        const int numIllegal = 10;
        const int totalFrames = num30Hz + num60Hz + num80Hz + num90Hz + num120Hz + numIllegal;

        // Add 30hz frames
        events.addAll(populateEvents(numberOfEvents: num30Hz,
                                      startTime: 0,
                                      interval: 32000000,
                                      margin: 0,
                                      ));

        // Add 60hz frames
        events.addAll(populateEvents(numberOfEvents: num60Hz,
                                      startTime: 0,
                                      interval: 16000000,
                                      margin: 0,
                                      ));

        // Add 80hz frames
        events.addAll(populateEvents(numberOfEvents: num80Hz,
                                      startTime: 0,
                                      interval: 12000000,
                                      margin: 0,
                                      ));

        // Add 90hz frames
        events.addAll(populateEvents(numberOfEvents: num90Hz,
                                      startTime: 0,
                                      interval: 11000000,
                                      margin: 0,
                                      ));

        // Add 120hz frames
        events.addAll(populateEvents(numberOfEvents: num120Hz,
                                      startTime: 0,
                                      interval: 8000000,
                                      margin: 0,
                                      ));

        // Add illegal refresh rate frames
        events.addAll(populateEvents(numberOfEvents: numIllegal,
                                      startTime: 0,
                                      interval: 60000,
                                      margin: 0,
                                      ));

        final RefreshRateSummary summary  = summarizeRefresh(events);

        expect(summary.percentageOf30HzFrames, closeTo(num30Hz/totalFrames*100, kCompareDelta));
        expect(summary.percentageOf60HzFrames, closeTo(num60Hz/totalFrames*100, kCompareDelta));
        expect(summary.percentageOf80HzFrames, closeTo(num80Hz/totalFrames*100, kCompareDelta));
        expect(summary.percentageOf90HzFrames, closeTo(num90Hz/totalFrames*100, kCompareDelta));
        expect(summary.percentageOf120HzFrames, closeTo(num120Hz/totalFrames*100, kCompareDelta));
        expect(summary.framesWithIllegalRefreshRate, isNotEmpty);
        expect(summary.framesWithIllegalRefreshRate.length, 10);
      });
    });
  });
}