Skip to content

Commit

Permalink
Prepare widget with data for versions downloads chart (dart-lang#8360)
Browse files Browse the repository at this point in the history
  • Loading branch information
szakarias authored Dec 5, 2024
1 parent a1a879c commit b15b995
Show file tree
Hide file tree
Showing 12 changed files with 378 additions and 29 deletions.
2 changes: 1 addition & 1 deletion app/lib/frontend/handlers/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ Future<PackagePageData> loadPackagePageData(
final scoreCardFuture = scoreCardBackend
.getScoreCardData(packageName, versionName, package: package);

final weeklyDownloadCountsFuture = getWeeklyDownloads(package.name!);
final weeklyDownloadCountsFuture = getWeeklyTotalDownloads(package.name!);

await Future.wait([
latestReleasesFuture,
Expand Down
37 changes: 37 additions & 0 deletions app/lib/frontend/templates/views/pkg/score_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:_pub_shared/format/encoding.dart';
import 'package:_pub_shared/format/number_format.dart';
import 'package:pana/models.dart';
import 'package:pub_dev/service/download_counts/download_counts.dart';
import 'package:pub_dev/shared/popularity_storage.dart';

import '../../../../scorecard/models.dart' hide ReportStatus;
Expand Down Expand Up @@ -77,6 +79,8 @@ d.Node scoreTabNode({
],
),
_reportNode(report),
if (card.weeklyVersionsDownloads != null)
_versionDownloadsChart(card.weeklyVersionsDownloads!),
if (toolEnvInfo != null) toolEnvInfo,
]),
if (!showPending)
Expand Down Expand Up @@ -173,6 +177,39 @@ d.Node _section(ReportSection section) {
]);
}

d.Node _versionDownloadsChart(
WeeklyVersionsDownloadCounts weeklyVersionsDownloads) {
return d.div(
classes: ['versions-downloads-chart'],
id: '-versions-downloads-chart',
attributes: {
'data-widget': 'versions-downloads-chart',
'data-versions-downloads-chart':
_encodeForVersionsChart(weeklyVersionsDownloads)
},
);
}

String _encodeForVersionsChart(WeeklyVersionsDownloadCounts wvcd) {
final date = wvcd.newestDate.toUtc().millisecondsSinceEpoch ~/ 1000;

final allCounts = <int>[];
final allRanges = <String>[];
wvcd.majorRangeWeeklyDownloads.forEach((e) => allCounts.addAll(e.counts));
wvcd.minorRangeWeeklyDownloads.forEach((e) => allCounts.addAll(e.counts));
wvcd.patchRangeWeeklyDownloads.forEach((e) => allCounts.addAll(e.counts));
allCounts.addAll(wvcd.totalWeeklyDownloads);

wvcd.majorRangeWeeklyDownloads.forEach((e) => allRanges.add(e.versionRange));
wvcd.minorRangeWeeklyDownloads.forEach((e) => allRanges.add(e.versionRange));
wvcd.patchRangeWeeklyDownloads.forEach((e) => allRanges.add(e.versionRange));

return [
encodeIntsAsLittleEndianBase64String([date, ...allCounts]),
allRanges
].join(',');
}

final _statusIconUrls = {
ReportStatus.passed:
staticUrls.getAssetUrl('/static/img/report-ok-icon-green.svg'),
Expand Down
5 changes: 5 additions & 0 deletions app/lib/scorecard/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:pool/pool.dart';
import 'package:pub_dev/service/download_counts/backend.dart';
import 'package:pub_dev/service/download_counts/computations.dart';
import 'package:pub_dev/shared/exceptions.dart';
import 'package:pub_dev/shared/popularity_storage.dart';
import 'package:pub_dev/task/backend.dart';
Expand Down Expand Up @@ -162,6 +163,9 @@ class ScoreCardBackend {
taskStatus = PackageVersionStatus.pending;
}

final weeklyVersionsDownloads =
await getWeeklyVersionsDownloads(packageName);

final data = ScoreCardData(
packageName: packageName,
packageVersion: packageVersion,
Expand All @@ -173,6 +177,7 @@ class ScoreCardBackend {
),
panaReport: PanaReport.fromSummary(summary, packageStatus: status),
taskStatus: taskStatus,
weeklyVersionsDownloads: weeklyVersionsDownloads,
);
await cacheEntry.set(data);
return data;
Expand Down
3 changes: 3 additions & 0 deletions app/lib/scorecard/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:_pub_shared/search/tags.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:pana/models.dart';
import 'package:pub_dev/service/download_counts/backend.dart';
import 'package:pub_dev/service/download_counts/download_counts.dart';
import 'package:pub_dev/task/models.dart';

import '../scorecard/backend.dart';
Expand All @@ -33,6 +34,7 @@ class ScoreCardData {
final DartdocReport? dartdocReport;
final PanaReport? panaReport;
final PackageVersionStatus? taskStatus;
final WeeklyVersionsDownloadCounts? weeklyVersionsDownloads;

ScoreCardData({
this.packageName,
Expand All @@ -42,6 +44,7 @@ class ScoreCardData {
this.dartdocReport,
this.panaReport,
this.taskStatus,
this.weeklyVersionsDownloads,
});

factory ScoreCardData.fromJson(Map<String, dynamic> json) =>
Expand Down
5 changes: 5 additions & 0 deletions app/lib/scorecard/models.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 66 additions & 19 deletions app/lib/service/download_counts/computations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,42 +46,89 @@ Future<void> upload30DaysTotal(Map<String, int> counts) async {
jsonUtf8Encoder.convert(counts));
}

Future<WeeklyDownloadCounts?> getWeeklyDownloads(String package) async {
Future<WeeklyDownloadCounts?> getWeeklyTotalDownloads(String package) async {
return (await cache.weeklyDownloadCounts(package).get(() async {
final wdc = await computeWeeklyDownloads(package);
if (wdc.newestDate == null) {
return null;
}
return WeeklyDownloadCounts(
weeklyDownloads: wdc.weeklyDownloads, newestDate: wdc.newestDate!);
return computeWeeklyTotalDownloads(package);
}));
}

Future<WeeklyVersionsDownloadCounts?> getWeeklyVersionsDownloads(
String package) async {
return (await cache.weeklyVersionsDownloadCounts(package).get(() async {
return computeWeeklyVersionsDownloads(package);
}));
}

/// Computes `weeklyDownloads` starting from `newestDate` for [package].
/// Computes weekly downloads starting from `newestDate` for [package] and 52
/// weeks back.
///
/// Each number in `weeklyDownloads` is the total number of downloads for
/// Each number in weeklyDownloads` is the total number of downloads for
/// a given 7 day period starting from the newest date with download counts
/// data available.
Future<({List<int> weeklyDownloads, DateTime? newestDate})>
computeWeeklyDownloads(String package) async {
final weeklyDownloads = List.filled(52, 0);
Future<WeeklyDownloadCounts?> computeWeeklyTotalDownloads(
String package) async {
final countData =
await downloadCountsBackend.lookupDownloadCountData(package);
if (countData == null) {
return (weeklyDownloads: <int>[], newestDate: null);
return null;
}

final totals = countData.totalCounts;
return WeeklyDownloadCounts(
weeklyDownloads: _computeWeeklyCounts(countData.totalCounts),
newestDate: countData.newestDate!);
}

/// Computes weekly downloads starting from `newestDate` for [package] and 52
/// weeks back for all stored major, minor, and patch version ranges and total
/// downloads.
Future<WeeklyVersionsDownloadCounts?> computeWeeklyVersionsDownloads(
String package) async {
final countData =
await downloadCountsBackend.lookupDownloadCountData(package);
if (countData == null) return null;

final majorRangeWeeklyCounts = <VersionRangeCount>[];
countData.majorRangeCounts.forEach((vrc) {
majorRangeWeeklyCounts.add((
counts: _computeWeeklyCounts(vrc.counts),
versionRange: vrc.versionRange
));
});
final minorRangeWeeklyCounts = <VersionRangeCount>[];
countData.minorRangeCounts.forEach((vrc) {
minorRangeWeeklyCounts.add((
counts: _computeWeeklyCounts(vrc.counts),
versionRange: vrc.versionRange
));
});
final patchRangeWeeklyCounts = <VersionRangeCount>[];
countData.patchRangeCounts.forEach((vrc) {
patchRangeWeeklyCounts.add((
counts: _computeWeeklyCounts(vrc.counts),
versionRange: vrc.versionRange
));
});

final weeklyTotalCounts = _computeWeeklyCounts(countData.totalCounts);

return WeeklyVersionsDownloadCounts(
newestDate: countData.newestDate!,
majorRangeWeeklyDownloads: majorRangeWeeklyCounts,
minorRangeWeeklyDownloads: minorRangeWeeklyCounts,
patchRangeWeeklyDownloads: patchRangeWeeklyCounts,
totalWeeklyDownloads: weeklyTotalCounts);
}

List<int> _computeWeeklyCounts(List<int> dailyCounts) {
final weeklyCounts = List.filled(52, 0);
for (int w = 0; w < 52; w++) {
var sum = 0;
for (int d = 0; d < 7; d++) {
if (totals[w * 7 + d] > 0) {
sum += totals[w * 7 + d];
if (dailyCounts[w * 7 + d] > 0) {
sum += dailyCounts[w * 7 + d];
}
}
weeklyDownloads[w] = sum;
weeklyCounts[w] = sum;
}

return (weeklyDownloads: weeklyDownloads, newestDate: countData.newestDate!);
return weeklyCounts;
}
65 changes: 60 additions & 5 deletions app/lib/service/download_counts/download_counts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:pub_semver/pub_semver.dart';
part 'download_counts.g.dart';

/// A tuple containing a string describing a `versionRange`, for instance
/// '>=1.0.0-0 <2.0.0', and a list of integers with a count for each day.
/// The `counts` list contains at most [maxAge] entries. The first entry
/// represents the number of downloads on `newestDate` followed by the
/// downloads on `newestDate` - 1 and so on. E.g.
/// A [VersionRangeCount] is a tuple containing a version range and a list of
/// download counts for periods of same length.
///
/// The first entry in the tuple is a string describing the `versionRange`, for
/// instance '>=1.0.0-0 <2.0.0'.
///
/// The second entry in the tuple is an integer list of `counts` with download
/// counts for each period. A period could for instance be a day, or a week etc.
/// The `counts` list contains at most [maxAge] entries.
///
/// Consider the example of period being one day. The first count represents the
/// number of downloads on `newestDate` followed by the downloads on
/// `newestDate` - 1 and so on. E.g.
///
/// counts = [ 42, 21, 55 ]
/// ▲ ▲ ▲
Expand Down Expand Up @@ -218,7 +226,11 @@ class CountData {

@JsonSerializable(includeIfNull: false)
class WeeklyDownloadCounts {
/// Each number in [weeklyDownloads] is the total number of downloads for
/// a given 7 day period starting from [newestDate].
final List<int> weeklyDownloads;

/// The newest date with download counts data available.
final DateTime newestDate;

WeeklyDownloadCounts({
Expand All @@ -230,3 +242,46 @@ class WeeklyDownloadCounts {
_$WeeklyDownloadCountsFromJson(json);
Map<String, dynamic> toJson() => _$WeeklyDownloadCountsToJson(this);
}

@JsonSerializable(includeIfNull: false)
class WeeklyVersionsDownloadCounts {
/// An integer list where each number is the total number of downloads for a
/// given 7 day period starting from [newestDate].
final List<int> totalWeeklyDownloads;

/// A list of [VersionRangeCount] with major version ranges and weekly
/// downloads for these ranges.
///
/// E.g. each number in the `counts` list is the total number of downloads for
/// the range in a 7 day period starting from [newestDate].
final List<VersionRangeCount> majorRangeWeeklyDownloads;

/// A list of [VersionRangeCount] with minor version ranges and weekly
/// downloads for these ranges.
///
/// E.g. each number in the `counts` list is the total number of downloads for
/// the range in a 7 day period starting from [newestDate].
final List<VersionRangeCount> minorRangeWeeklyDownloads;

/// A list of [VersionRangeCount] with patch version ranges and weekly
/// downloads for these ranges.
///
/// E.g. each number in the `counts` list is the total number of downloads for
/// the range in a 7 day period starting from [newestDate].
final List<VersionRangeCount> patchRangeWeeklyDownloads;

/// The newest date with download counts data available.
final DateTime newestDate;

WeeklyVersionsDownloadCounts({
required this.newestDate,
required this.majorRangeWeeklyDownloads,
required this.minorRangeWeeklyDownloads,
required this.patchRangeWeeklyDownloads,
required this.totalWeeklyDownloads,
});

factory WeeklyVersionsDownloadCounts.fromJson(Map<String, dynamic> json) =>
_$WeeklyVersionsDownloadCountsFromJson(json);
Map<String, dynamic> toJson() => _$WeeklyVersionsDownloadCountsToJson(this);
}
Loading

0 comments on commit b15b995

Please sign in to comment.