From 7d9f2793828306c93ac3d01fb458bb2daa50d8ec Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Tue, 31 Jul 2018 15:46:36 +0200 Subject: [PATCH 1/7] Add runtimeVersion to the ScoreCard models and ids. --- app/lib/scorecard/models.dart | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/app/lib/scorecard/models.dart b/app/lib/scorecard/models.dart index 13c56448cd..2347e1dad2 100644 --- a/app/lib/scorecard/models.dart +++ b/app/lib/scorecard/models.dart @@ -11,10 +11,15 @@ import 'package:meta/meta.dart'; import 'package:pub_dartlang_org/search/scoring.dart' show calculateOverallScore; +import '../frontend/models.dart' show Package, PackageVersion; import '../shared/model_properties.dart'; +import '../shared/versions.dart' as versions; -String _id(String packageName, String packageVersion) => - '$packageName/$packageVersion'; +db.Key _pvKey(String packageName, String packageVersion) { + return db.dbService.emptyKey + .append(Package, id: packageName) + .append(PackageVersion, id: packageVersion); +} final _gzipCodec = new GZipCodec(); @@ -27,6 +32,9 @@ class ScoreCard extends db.ExpandoModel { @db.StringProperty(required: true) String packageVersion; + @db.StringProperty(required: true) + String runtimeVersion; + @db.DateTimeProperty(required: true) DateTime packageCreated; @@ -65,7 +73,9 @@ class ScoreCard extends db.ExpandoModel { @required this.packageCreated, @required this.packageVersionCreated, }) { - id = _id(packageName, packageVersion); + parentKey = _pvKey(packageName, packageVersion); + runtimeVersion = versions.runtimeVersion; + id = runtimeVersion; } double get overallScore => @@ -86,6 +96,9 @@ class ScoreCardReport extends db.ExpandoModel { @db.StringProperty(required: true) String packageVersion; + @db.StringProperty(required: true) + String runtimeVersion; + @db.StringProperty(required: true) String reportType; @@ -99,8 +112,9 @@ class ScoreCardReport extends db.ExpandoModel { @required this.packageVersion, @required this.reportType, }) { - parentKey = db.dbService.emptyKey - .append(ScoreCard, id: _id(packageName, packageVersion)); + runtimeVersion = versions.runtimeVersion; + parentKey = _pvKey(packageName, packageVersion) + .append(ScoreCard, id: runtimeVersion); id = reportType; } From d76481aadec1adf9463049e0b17934ece395dc4c Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Tue, 31 Jul 2018 16:17:55 +0200 Subject: [PATCH 2/7] Simplify top-level ScoreCard model, the report-specific details are removed, flags are handled as a List of values. --- app/lib/scorecard/models.dart | 44 +++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/app/lib/scorecard/models.dart b/app/lib/scorecard/models.dart index 2347e1dad2..2d6db9e18d 100644 --- a/app/lib/scorecard/models.dart +++ b/app/lib/scorecard/models.dart @@ -23,6 +23,11 @@ db.Key _pvKey(String packageName, String packageVersion) { final _gzipCodec = new GZipCodec(); +abstract class PackageFlags { + static const String doNotAdvertise = 'do-not-adverise'; + static const String isDiscontinued = 'discontinued'; +} + /// Summary of various reports for a given PackageVersion. @db.Kind(name: 'ScoreCard', idType: db.IdType.String) class ScoreCard extends db.ExpandoModel { @@ -41,18 +46,6 @@ class ScoreCard extends db.ExpandoModel { @db.DateTimeProperty(required: true) DateTime packageVersionCreated; - /// Whether the package has its discontinued flag set. - @db.BoolProperty() - bool isDiscontinued; - - /// The platform tags (flutter, web, other) set by `pana` analysis. - @CompatibleStringListProperty() - List panaPlatformTags; - - /// Score for documentation coverage (0.0 - 1.0). - @db.DoubleProperty() - double documentationScore; - /// Score for code health (0.0 - 1.0). @db.DoubleProperty() double healthScore; @@ -65,6 +58,14 @@ class ScoreCard extends db.ExpandoModel { @db.DoubleProperty() double popularityScore; + /// The platform tags (flutter, web, other). + @CompatibleStringListProperty() + List platformTags; + + /// The flags for the package, version or analysis. + @CompatibleStringListProperty() + List flags; + ScoreCard(); ScoreCard.init({ @@ -85,6 +86,25 @@ class ScoreCard extends db.ExpandoModel { maintenance: maintenanceScore ?? 0.0, popularity: popularityScore ?? 0.0, ); + + bool get isNew => new DateTime.now().difference(packageCreated).inDays <= 30; + + bool get isDiscontinued => + flags != null && flags.contains(PackageFlags.isDiscontinued); + + bool get doNotAdvertise => + flags != null && flags.contains(PackageFlags.doNotAdvertise); + + void addFlag(String flag) { + flags ??= []; + if (!flags.contains(flag)) { + flags.add(flag); + } + } + + void removeFlag(String flag) { + flags?.remove(flag); + } } /// Detail of a specific report for a given PackageVersion. From 7e0534bd98eefef943a478761da7be191d2196a4 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 2 Aug 2018 12:03:22 +0200 Subject: [PATCH 3/7] ScoreCard backend. --- app/build.yaml | 1 + app/lib/scorecard/backend.dart | 156 +++++++++++++++++++++++++++++++ app/lib/scorecard/helpers.dart | 19 ++++ app/lib/scorecard/models.dart | 160 +++++++++++++++++++++++++++++--- app/lib/scorecard/models.g.dart | 74 +++++++++++++++ 5 files changed, 399 insertions(+), 11 deletions(-) create mode 100644 app/lib/scorecard/backend.dart create mode 100644 app/lib/scorecard/helpers.dart create mode 100644 app/lib/scorecard/models.g.dart diff --git a/app/build.yaml b/app/build.yaml index 8c1cb2706f..cacc7beff4 100644 --- a/app/build.yaml +++ b/app/build.yaml @@ -5,6 +5,7 @@ targets: sources: - 'lib/history/models.dart' - 'lib/dartdoc/models.dart' + - 'lib/scorecard/models.dart' - 'lib/shared/*.dart' - 'lib/search/backend*.dart' builders: diff --git a/app/lib/scorecard/backend.dart b/app/lib/scorecard/backend.dart new file mode 100644 index 0000000000..77c8efd536 --- /dev/null +++ b/app/lib/scorecard/backend.dart @@ -0,0 +1,156 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// 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 'dart:async'; + +import 'package:gcloud/db.dart' as db; +import 'package:gcloud/service_scope.dart' as ss; +import 'package:logging/logging.dart'; + +import '../frontend/models.dart' show Package, PackageVersion; +import '../shared/popularity_storage.dart'; +import '../shared/utils.dart'; +import '../shared/versions.dart' as versions; + +import 'helpers.dart'; +import 'models.dart'; + +export 'models.dart'; + +final _logger = new Logger('pub.scorecard.backend'); + +/// Sets the active scorecard backend. +void registerScoreCardBackend(ScoreCardBackend backend) => + ss.register(#_scorecard_backend, backend); + +/// The active job backend. +ScoreCardBackend get scoreCardBackend => + ss.lookup(#_scorecard_backend) as ScoreCardBackend; + +class ScoreCardBackend { + final db.DatastoreDB _db; + ScoreCardBackend(this._db); + + Future latestScoreCard( + String packageName, String packageVersion) async { + final key = scoreCardKey(packageName, packageVersion); + final currentList = await _db.lookup([key]); + if (currentList.first != null) { + return currentList.first as ScoreCard; + } + + final query = _db.query(ScoreCard, ancestorKey: key.parent) + ..filter('<', versions.runtimeVersion); + final all = await query + .run() + .cast() + .where((sc) => + // sanity check to not rely entirely on the lexicographical order + isNewer(sc.semanticRuntimeVersion, versions.semanticRuntimeVersion)) + .toList(); + if (all.isEmpty) { + return null; + } + all.sort((a, b) => + isNewer(a.semanticRuntimeVersion, b.semanticRuntimeVersion) ? -1 : 1); + return all.last; + } + + Future updateReport( + String packageName, String packageVersion, ReportData data) async { + final key = scoreCardKey(packageName, packageVersion) + .append(ScoreCardReport, id: data.reportType); + await _db.withTransaction((tx) async { + ScoreCardReport report; + final reportList = await tx.lookup([key]); + report = reportList.first as ScoreCardReport; + if (report != null) { + _logger.info( + 'Updating report: $packageName $packageVersion ${data.reportType}.'); + report + ..updated = new DateTime.now().toUtc() + ..reportStatus = data.reportStatus + ..reportJson = data.toJson(); + } else { + _logger.info( + 'Creating new report: $packageName $packageVersion ${data.reportType}.'); + report = new ScoreCardReport.init( + packageName: packageName, + packageVersion: packageVersion, + reportData: data, + ); + } + tx.queueMutations(inserts: [report]); + await tx.commit(); + }); + } + + Future> loadReports( + String packageName, String packageVersion, + {List reportTypes}) async { + reportTypes ??= [ReportType.pana, ReportType.dartdoc]; + final key = scoreCardKey(packageName, packageVersion); + + final list = await _db.lookup(reportTypes + .map((type) => key.append(ScoreCardReport, id: type)) + .toList()); + + final result = {}; + for (db.Model model in list) { + if (model == null) continue; + final report = model as ScoreCardReport; + result[report.reportType] = report.reportData; + } + return result; + } + + Future updateScoreCard(String packageName, String packageVersion) async { + final key = scoreCardKey(packageName, packageVersion); + final pAndPv = await _db.lookup([key.parent, key.parent.parent]); + final package = pAndPv[0] as Package; + final version = pAndPv[1] as PackageVersion; + if (package == null || version == null) { + throw new Exception('Unable to lookup $packageName $packageVersion.'); + } + + final reports = await loadReports(packageName, packageVersion); + + await _db.withTransaction((tx) async { + ScoreCard scoreCard; + final scoreCardList = await tx.lookup([key]); + scoreCard = scoreCardList.first as ScoreCard; + + if (scoreCard == null) { + _logger.info('Creating new ScoreCard $packageName $packageVersion.'); + scoreCard = new ScoreCard.init( + packageName: packageName, + packageVersion: packageVersion, + packageCreated: package.created, + packageVersionCreated: version.created, + ); + } else { + _logger.info('Updating ScoreCard $packageName $packageVersion.'); + scoreCard.updated = new DateTime.now().toUtc(); + } + + scoreCard.flags = null; + if (package.isDiscontinued ?? false) { + scoreCard.addFlag(PackageFlags.isDiscontinued); + } + if (package.doNotAdvertise ?? false) { + scoreCard.addFlag(PackageFlags.doNotAdvertise); + } + + scoreCard.popularityScore = popularityStorage.lookup(packageName) ?? 0.0; + + scoreCard.updateFromReports( + panaReport: reports[ReportType.pana] as PanaReport, + dartdocReport: reports[ReportType.dartdoc] as DartdocReport, + ); + + tx.queueMutations(inserts: [scoreCard]); + await tx.commit(); + }); + } +} diff --git a/app/lib/scorecard/helpers.dart b/app/lib/scorecard/helpers.dart new file mode 100644 index 0000000000..0a0443e14d --- /dev/null +++ b/app/lib/scorecard/helpers.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// 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:gcloud/db.dart' as db; + +import '../frontend/models.dart' show Package, PackageVersion; +import '../shared/versions.dart' as versions; + +db.Key scoreCardKey( + String packageName, + String packageVersion, { + String runtimeVersion, +}) { + runtimeVersion ??= versions.runtimeVersion; + return db.dbService.emptyKey + .append(Package, id: packageName) + .append(PackageVersion, id: packageVersion); +} diff --git a/app/lib/scorecard/models.dart b/app/lib/scorecard/models.dart index 2d6db9e18d..48247db0b6 100644 --- a/app/lib/scorecard/models.dart +++ b/app/lib/scorecard/models.dart @@ -6,20 +6,23 @@ import 'dart:convert'; import 'dart:io'; import 'package:gcloud/db.dart' as db; +import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; +import 'package:pana/models.dart' show Suggestion, PkgDependency; +import 'package:pub_semver/pub_semver.dart'; import 'package:pub_dartlang_org/search/scoring.dart' show calculateOverallScore; -import '../frontend/models.dart' show Package, PackageVersion; import '../shared/model_properties.dart'; import '../shared/versions.dart' as versions; -db.Key _pvKey(String packageName, String packageVersion) { - return db.dbService.emptyKey - .append(Package, id: packageName) - .append(PackageVersion, id: packageVersion); -} +import 'helpers.dart'; + +export 'package:pana/models.dart' + show Suggestion, SuggestionCode, SuggestionLevel; + +part 'models.g.dart'; final _gzipCodec = new GZipCodec(); @@ -28,6 +31,17 @@ abstract class PackageFlags { static const String isDiscontinued = 'discontinued'; } +abstract class ReportType { + static const String pana = 'pana'; + static const String dartdoc = 'dartdoc'; +} + +abstract class ReportStatus { + static const String success = 'success'; + static const String failed = 'failed'; + static const String aborted = 'aborted'; +} + /// Summary of various reports for a given PackageVersion. @db.Kind(name: 'ScoreCard', idType: db.IdType.String) class ScoreCard extends db.ExpandoModel { @@ -40,6 +54,9 @@ class ScoreCard extends db.ExpandoModel { @db.StringProperty(required: true) String runtimeVersion; + @db.DateTimeProperty() + DateTime updated; + @db.DateTimeProperty(required: true) DateTime packageCreated; @@ -66,6 +83,10 @@ class ScoreCard extends db.ExpandoModel { @CompatibleStringListProperty() List flags; + /// The report types that are already done for the ScoreCard. + @CompatibleStringListProperty() + List reportTypes; + ScoreCard(); ScoreCard.init({ @@ -74,9 +95,11 @@ class ScoreCard extends db.ExpandoModel { @required this.packageCreated, @required this.packageVersionCreated, }) { - parentKey = _pvKey(packageName, packageVersion); runtimeVersion = versions.runtimeVersion; - id = runtimeVersion; + final key = scoreCardKey(packageName, packageVersion); + parentKey = key.parent; + id = key.id; + updated = new DateTime.now().toUtc(); } double get overallScore => @@ -95,6 +118,10 @@ class ScoreCard extends db.ExpandoModel { bool get doNotAdvertise => flags != null && flags.contains(PackageFlags.doNotAdvertise); + Version get semanticRuntimeVersion => new Version.parse(runtimeVersion); + + bool get isCurrentRuntimeVersion => runtimeVersion == versions.runtimeVersion; + void addFlag(String flag) { flags ??= []; if (!flags.contains(flag)) { @@ -105,6 +132,22 @@ class ScoreCard extends db.ExpandoModel { void removeFlag(String flag) { flags?.remove(flag); } + + void updateFromReports({ + PanaReport panaReport, + DartdocReport dartdocReport, + }) { + healthScore = (panaReport?.healthScore ?? 0.0) * + (0.9 + ((dartdocReport?.coverageScore ?? 1.0) * 0.1)); + maintenanceScore = panaReport?.maintenanceScore ?? 0.0; + platformTags = panaReport?.platformTags; + reportTypes = [ + panaReport == null ? null : ReportType.pana, + dartdocReport == null ? null : ReportType.dartdoc, + ] + ..removeWhere((type) => type == null) + ..sort(); + } } /// Detail of a specific report for a given PackageVersion. @@ -122,6 +165,12 @@ class ScoreCardReport extends db.ExpandoModel { @db.StringProperty(required: true) String reportType; + @db.DateTimeProperty() + DateTime updated; + + @db.StringProperty(required: true) + String reportStatus; + @db.BlobProperty() List reportJsonGz; @@ -130,12 +179,15 @@ class ScoreCardReport extends db.ExpandoModel { ScoreCardReport.init({ @required this.packageName, @required this.packageVersion, - @required this.reportType, + @required ReportData reportData, }) { runtimeVersion = versions.runtimeVersion; - parentKey = _pvKey(packageName, packageVersion) - .append(ScoreCard, id: runtimeVersion); + parentKey = scoreCardKey(packageName, packageVersion); + reportType = reportData.reportType; + reportStatus = reportData.reportStatus; id = reportType; + updated = new DateTime.now().toUtc(); + reportJson = reportData.toJson(); } Map get reportJson { @@ -151,4 +203,90 @@ class ScoreCardReport extends db.ExpandoModel { reportJsonGz = _gzipCodec.encode(utf8.encode(json.encode(map))); } } + + ReportData get reportData { + switch (reportType) { + case ReportType.pana: + return new PanaReport.fromJson(reportJson); + case ReportType.dartdoc: + return new DartdocReport.fromJson(reportJson); + } + throw new Exception('Unknown report type: $reportType'); + } +} + +abstract class ReportData { + String get reportType; + String get reportStatus; + Map toJson(); +} + +@JsonSerializable() +class PanaReport extends Object + with _$PanaReportSerializerMixin + implements ReportData { + @override + String get reportType => ReportType.pana; + + @override + final String reportStatus; + + @override + final double healthScore; + + @override + final double maintenanceScore; + + /// The platform tags (flutter, web, other). + @CompatibleStringListProperty() + @override + List platformTags; + + @override + final String platformReason; + + @override + final List pkgDependencies; + + @override + final List suggestions; + + PanaReport({ + @required this.reportStatus, + @required this.healthScore, + @required this.maintenanceScore, + @required this.platformTags, + @required this.platformReason, + @required this.pkgDependencies, + @required this.suggestions, + }); + + factory PanaReport.fromJson(Map json) => + _$PanaReportFromJson(json); +} + +@JsonSerializable() +class DartdocReport extends Object + with _$DartdocReportSerializerMixin + implements ReportData { + @override + String get reportType => ReportType.dartdoc; + + @override + final String reportStatus; + + @override + final double coverageScore; + + @override + final List suggestions; + + DartdocReport({ + @required this.reportStatus, + @required this.coverageScore, + @required this.suggestions, + }); + + factory DartdocReport.fromJson(Map json) => + _$DartdocReportFromJson(json); } diff --git a/app/lib/scorecard/models.g.dart b/app/lib/scorecard/models.g.dart new file mode 100644 index 0000000000..8509424568 --- /dev/null +++ b/app/lib/scorecard/models.g.dart @@ -0,0 +1,74 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// 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. + +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: prefer_final_locals + +part of 'models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PanaReport _$PanaReportFromJson(Map json) { + return new PanaReport( + reportStatus: json['reportStatus'] as String, + healthScore: (json['healthScore'] as num)?.toDouble(), + maintenanceScore: (json['maintenanceScore'] as num)?.toDouble(), + platformTags: + (json['platformTags'] as List)?.map((e) => e as String)?.toList(), + platformReason: json['platformReason'] as String, + pkgDependencies: (json['pkgDependencies'] as List) + ?.map((e) => e == null + ? null + : new PkgDependency.fromJson(e as Map)) + ?.toList(), + suggestions: (json['suggestions'] as List) + ?.map((e) => e == null + ? null + : new Suggestion.fromJson(e as Map)) + ?.toList()); +} + +abstract class _$PanaReportSerializerMixin { + String get reportStatus; + double get healthScore; + double get maintenanceScore; + List get platformTags; + String get platformReason; + List get pkgDependencies; + List get suggestions; + Map toJson() => { + 'reportStatus': reportStatus, + 'healthScore': healthScore, + 'maintenanceScore': maintenanceScore, + 'platformTags': platformTags, + 'platformReason': platformReason, + 'pkgDependencies': pkgDependencies, + 'suggestions': suggestions + }; +} + +DartdocReport _$DartdocReportFromJson(Map json) { + return new DartdocReport( + reportStatus: json['reportStatus'] as String, + coverageScore: (json['coverageScore'] as num)?.toDouble(), + suggestions: (json['suggestions'] as List) + ?.map((e) => e == null + ? null + : new Suggestion.fromJson(e as Map)) + ?.toList()); +} + +abstract class _$DartdocReportSerializerMixin { + String get reportStatus; + double get coverageScore; + List get suggestions; + Map toJson() => { + 'reportStatus': reportStatus, + 'coverageScore': coverageScore, + 'suggestions': suggestions + }; +} From 879f260d60df7a65a8f34f441bc88757c512866e Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 2 Aug 2018 21:25:30 +0200 Subject: [PATCH 4/7] Memcache for ScoreCardData --- app/lib/scorecard/backend.dart | 31 ++++++- app/lib/scorecard/models.dart | 105 ++++++++++++++++++---- app/lib/scorecard/models.g.dart | 53 +++++++++++ app/lib/scorecard/scorecard_memcache.dart | 74 +++++++++++++++ app/lib/shared/memcache.dart | 2 + 5 files changed, 245 insertions(+), 20 deletions(-) create mode 100644 app/lib/scorecard/scorecard_memcache.dart diff --git a/app/lib/scorecard/backend.dart b/app/lib/scorecard/backend.dart index 77c8efd536..c1fb479d77 100644 --- a/app/lib/scorecard/backend.dart +++ b/app/lib/scorecard/backend.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:gcloud/db.dart' as db; import 'package:gcloud/service_scope.dart' as ss; import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import '../frontend/models.dart' show Package, PackageVersion; import '../shared/popularity_storage.dart'; @@ -15,6 +16,7 @@ import '../shared/versions.dart' as versions; import 'helpers.dart'; import 'models.dart'; +import 'scorecard_memcache.dart'; export 'models.dart'; @@ -32,12 +34,28 @@ class ScoreCardBackend { final db.DatastoreDB _db; ScoreCardBackend(this._db); - Future latestScoreCard( - String packageName, String packageVersion) async { + Future getScoreCardData( + String packageName, + String packageVersion, { + @required bool onlyCurrent, + }) async { + final cached = await scoreCardMemcache.getScoreCardData( + packageName, packageVersion, versions.runtimeVersion, + onlyCurrent: onlyCurrent); + if (cached != null) { + return cached; + } + final key = scoreCardKey(packageName, packageVersion); final currentList = await _db.lookup([key]); if (currentList.first != null) { - return currentList.first as ScoreCard; + final data = (currentList.first as ScoreCard).toData(); + await scoreCardMemcache.setScoreCardData(data); + return data; + } + + if (onlyCurrent) { + return null; } final query = _db.query(ScoreCard, ancestorKey: key.parent) @@ -54,7 +72,9 @@ class ScoreCardBackend { } all.sort((a, b) => isNewer(a.semanticRuntimeVersion, b.semanticRuntimeVersion) ? -1 : 1); - return all.last; + final data = all.last.toData(); + await scoreCardMemcache.setScoreCardData(data); + return data; } Future updateReport( @@ -152,5 +172,8 @@ class ScoreCardBackend { tx.queueMutations(inserts: [scoreCard]); await tx.commit(); }); + + scoreCardMemcache.invalidate( + packageName, packageVersion, versions.runtimeVersion); } } diff --git a/app/lib/scorecard/models.dart b/app/lib/scorecard/models.dart index 48247db0b6..fbbbe956b7 100644 --- a/app/lib/scorecard/models.dart +++ b/app/lib/scorecard/models.dart @@ -102,26 +102,23 @@ class ScoreCard extends db.ExpandoModel { updated = new DateTime.now().toUtc(); } - double get overallScore => - // TODO: use documentationScore too - calculateOverallScore( - health: healthScore ?? 0.0, - maintenance: maintenanceScore ?? 0.0, - popularity: popularityScore ?? 0.0, + ScoreCardData toData() => new ScoreCardData( + packageName: packageName, + packageVersion: packageVersion, + runtimeVersion: runtimeVersion, + updated: updated, + packageCreated: packageCreated, + packageVersionCreated: packageVersionCreated, + healthScore: healthScore, + maintenanceScore: maintenanceScore, + popularityScore: popularityScore, + platformTags: platformTags, + flags: flags, + reportTypes: reportTypes, ); - bool get isNew => new DateTime.now().difference(packageCreated).inDays <= 30; - - bool get isDiscontinued => - flags != null && flags.contains(PackageFlags.isDiscontinued); - - bool get doNotAdvertise => - flags != null && flags.contains(PackageFlags.doNotAdvertise); - Version get semanticRuntimeVersion => new Version.parse(runtimeVersion); - bool get isCurrentRuntimeVersion => runtimeVersion == versions.runtimeVersion; - void addFlag(String flag) { flags ??= []; if (!flags.contains(flag)) { @@ -215,6 +212,82 @@ class ScoreCardReport extends db.ExpandoModel { } } +@JsonSerializable() +class ScoreCardData extends Object with _$ScoreCardDataSerializerMixin { + @override + final String packageName; + @override + final String packageVersion; + @override + final String runtimeVersion; + @override + final DateTime updated; + @override + final DateTime packageCreated; + @override + final DateTime packageVersionCreated; + + /// Score for code health (0.0 - 1.0). + @override + final double healthScore; + + /// Score for package maintenance (0.0 - 1.0). + @override + final double maintenanceScore; + + /// Score for package popularity (0.0 - 1.0). + @override + final double popularityScore; + + /// The platform tags (flutter, web, other). + @override + final List platformTags; + + /// The flags for the package, version or analysis. + @override + final List flags; + + /// The report types that are already done for the ScoreCard. + @override + final List reportTypes; + + ScoreCardData({ + @required this.packageName, + @required this.packageVersion, + @required this.runtimeVersion, + @required this.updated, + @required this.packageCreated, + @required this.packageVersionCreated, + @required this.healthScore, + @required this.maintenanceScore, + @required this.popularityScore, + @required this.platformTags, + @required this.flags, + @required this.reportTypes, + }); + + factory ScoreCardData.fromJson(Map json) => + _$ScoreCardDataFromJson(json); + + double get overallScore => + // TODO: use documentationScore too + calculateOverallScore( + health: healthScore ?? 0.0, + maintenance: maintenanceScore ?? 0.0, + popularity: popularityScore ?? 0.0, + ); + + bool get isNew => new DateTime.now().difference(packageCreated).inDays <= 30; + + bool get isDiscontinued => + flags != null && flags.contains(PackageFlags.isDiscontinued); + + bool get doNotAdvertise => + flags != null && flags.contains(PackageFlags.doNotAdvertise); + + bool get isCurrent => runtimeVersion == versions.runtimeVersion; +} + abstract class ReportData { String get reportType; String get reportStatus; diff --git a/app/lib/scorecard/models.g.dart b/app/lib/scorecard/models.g.dart index 8509424568..1135664e33 100644 --- a/app/lib/scorecard/models.g.dart +++ b/app/lib/scorecard/models.g.dart @@ -12,6 +12,59 @@ part of 'models.dart'; // JsonSerializableGenerator // ************************************************************************** +ScoreCardData _$ScoreCardDataFromJson(Map json) { + return new ScoreCardData( + packageName: json['packageName'] as String, + packageVersion: json['packageVersion'] as String, + runtimeVersion: json['runtimeVersion'] as String, + updated: json['updated'] == null + ? null + : DateTime.parse(json['updated'] as String), + packageCreated: json['packageCreated'] == null + ? null + : DateTime.parse(json['packageCreated'] as String), + packageVersionCreated: json['packageVersionCreated'] == null + ? null + : DateTime.parse(json['packageVersionCreated'] as String), + healthScore: (json['healthScore'] as num)?.toDouble(), + maintenanceScore: (json['maintenanceScore'] as num)?.toDouble(), + popularityScore: (json['popularityScore'] as num)?.toDouble(), + platformTags: + (json['platformTags'] as List)?.map((e) => e as String)?.toList(), + flags: (json['flags'] as List)?.map((e) => e as String)?.toList(), + reportTypes: + (json['reportTypes'] as List)?.map((e) => e as String)?.toList()); +} + +abstract class _$ScoreCardDataSerializerMixin { + String get packageName; + String get packageVersion; + String get runtimeVersion; + DateTime get updated; + DateTime get packageCreated; + DateTime get packageVersionCreated; + double get healthScore; + double get maintenanceScore; + double get popularityScore; + List get platformTags; + List get flags; + List get reportTypes; + Map toJson() => { + 'packageName': packageName, + 'packageVersion': packageVersion, + 'runtimeVersion': runtimeVersion, + 'updated': updated?.toIso8601String(), + 'packageCreated': packageCreated?.toIso8601String(), + 'packageVersionCreated': packageVersionCreated?.toIso8601String(), + 'healthScore': healthScore, + 'maintenanceScore': maintenanceScore, + 'popularityScore': popularityScore, + 'platformTags': platformTags, + 'flags': flags, + 'reportTypes': reportTypes + }; +} + PanaReport _$PanaReportFromJson(Map json) { return new PanaReport( reportStatus: json['reportStatus'] as String, diff --git a/app/lib/scorecard/scorecard_memcache.dart b/app/lib/scorecard/scorecard_memcache.dart new file mode 100644 index 0000000000..2697e41d6c --- /dev/null +++ b/app/lib/scorecard/scorecard_memcache.dart @@ -0,0 +1,74 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// 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 'dart:async'; +import 'dart:convert' as convert; + +import 'package:gcloud/service_scope.dart' as ss; +import 'package:logging/logging.dart'; +import 'package:memcache/memcache.dart'; +import 'package:meta/meta.dart'; + +import '../shared/memcache.dart'; + +import 'models.dart'; + +final Logger _logger = new Logger('pub.scorecard_memcache'); + +/// Sets the ScoreCard memcache. +void registerScoreCardMemcache(ScoreCardMemcache value) => + ss.register(#_scoreCardMemcache, value); + +/// The active ScoreCard memcache. +ScoreCardMemcache get scoreCardMemcache => + ss.lookup(#_scoreCardMemcache) as ScoreCardMemcache; + +class ScoreCardMemcache { + final SimpleMemcache _data; + + ScoreCardMemcache(Memcache memcache) + : _data = new SimpleMemcache( + _logger, + memcache, + scoreCardDataPrefix, + scoreCardDataExpiration, + ); + + Future getScoreCardData( + String packageName, + String packageVersion, + String runtimeVersion, { + @required bool onlyCurrent, + }) async { + String content = await _data + .getText(_dataKey(packageName, packageVersion, runtimeVersion, true)); + if (content == null && !onlyCurrent) { + content = await _data.getText( + _dataKey(packageName, packageVersion, runtimeVersion, false)); + } + if (content == null) { + return null; + } + return new ScoreCardData.fromJson( + convert.json.decode(content) as Map); + } + + Future setScoreCardData(ScoreCardData data) { + return _data.setText( + _dataKey(data.packageName, data.packageVersion, data.runtimeVersion, + data.isCurrent), + convert.json.encode(data.toJson())); + } + + Future invalidate(String package, String version, String runtimeVersion) { + return Future.wait([ + _data.invalidate(_dataKey(package, version, runtimeVersion, true)), + _data.invalidate(_dataKey(package, version, runtimeVersion, false)), + ]); + } + + String _dataKey(String package, String version, String runtimeVersion, + bool isCurrent) => + '/$package/$version/$runtimeVersion/$isCurrent'; +} diff --git a/app/lib/shared/memcache.dart b/app/lib/shared/memcache.dart index 1fee626094..f437414b55 100644 --- a/app/lib/shared/memcache.dart +++ b/app/lib/shared/memcache.dart @@ -16,6 +16,7 @@ const Duration analyzerDataExpiration = const Duration(minutes: 60); const Duration analyzerDataLocalExpiration = const Duration(minutes: 15); const Duration dartdocEntryExpiration = const Duration(hours: 24); const Duration dartdocFileInfoExpiration = const Duration(minutes: 60); +const Duration scoreCardDataExpiration = const Duration(minutes: 60); const Duration searchServiceResultExpiration = const Duration(minutes: 10); const Duration _memcacheRequestTimeout = const Duration(seconds: 5); @@ -26,6 +27,7 @@ const String analyzerDataPrefix = 'v2_dart_analyzer_api_'; const String analyzerExtractPrefix = 'v2_dart_analyzer_extract_'; const String dartdocEntryPrefix = 'dartdoc_entry_'; const String dartdocFileInfoPrefix = 'dartdoc_fileinfo_'; +const String scoreCardDataPrefix = 'scorecard_'; const String searchServiceResultPrefix = 'search_service_result_'; class SimpleMemcache { From 7c0b9adfb8a9c7259d876725ad9789cc5cd54b24 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Tue, 28 Aug 2018 10:12:13 +0200 Subject: [PATCH 5/7] Update to 2.0 and update generated code. --- app/lib/scorecard/backend.dart | 3 +- app/lib/scorecard/models.dart | 38 ++++------- app/lib/scorecard/models.g.dart | 112 ++++++++++++-------------------- 3 files changed, 52 insertions(+), 101 deletions(-) diff --git a/app/lib/scorecard/backend.dart b/app/lib/scorecard/backend.dart index c1fb479d77..0ced167452 100644 --- a/app/lib/scorecard/backend.dart +++ b/app/lib/scorecard/backend.dart @@ -58,11 +58,10 @@ class ScoreCardBackend { return null; } - final query = _db.query(ScoreCard, ancestorKey: key.parent) + final query = _db.query(ancestorKey: key.parent) ..filter('<', versions.runtimeVersion); final all = await query .run() - .cast() .where((sc) => // sanity check to not rely entirely on the lexicographical order isNewer(sc.semanticRuntimeVersion, versions.semanticRuntimeVersion)) diff --git a/app/lib/scorecard/models.dart b/app/lib/scorecard/models.dart index fbbbe956b7..6e98177642 100644 --- a/app/lib/scorecard/models.dart +++ b/app/lib/scorecard/models.dart @@ -213,42 +213,30 @@ class ScoreCardReport extends db.ExpandoModel { } @JsonSerializable() -class ScoreCardData extends Object with _$ScoreCardDataSerializerMixin { - @override +class ScoreCardData { final String packageName; - @override final String packageVersion; - @override final String runtimeVersion; - @override final DateTime updated; - @override final DateTime packageCreated; - @override final DateTime packageVersionCreated; /// Score for code health (0.0 - 1.0). - @override final double healthScore; /// Score for package maintenance (0.0 - 1.0). - @override final double maintenanceScore; /// Score for package popularity (0.0 - 1.0). - @override final double popularityScore; /// The platform tags (flutter, web, other). - @override final List platformTags; /// The flags for the package, version or analysis. - @override final List flags; /// The report types that are already done for the ScoreCard. - @override final List reportTypes; ScoreCardData({ @@ -286,6 +274,8 @@ class ScoreCardData extends Object with _$ScoreCardDataSerializerMixin { flags != null && flags.contains(PackageFlags.doNotAdvertise); bool get isCurrent => runtimeVersion == versions.runtimeVersion; + + Map toJson() => _$ScoreCardDataToJson(this); } abstract class ReportData { @@ -295,33 +285,25 @@ abstract class ReportData { } @JsonSerializable() -class PanaReport extends Object - with _$PanaReportSerializerMixin - implements ReportData { +class PanaReport implements ReportData { @override String get reportType => ReportType.pana; @override final String reportStatus; - @override final double healthScore; - @override final double maintenanceScore; /// The platform tags (flutter, web, other). @CompatibleStringListProperty() - @override List platformTags; - @override final String platformReason; - @override final List pkgDependencies; - @override final List suggestions; PanaReport({ @@ -336,22 +318,21 @@ class PanaReport extends Object factory PanaReport.fromJson(Map json) => _$PanaReportFromJson(json); + + @override + Map toJson() => _$PanaReportToJson(this); } @JsonSerializable() -class DartdocReport extends Object - with _$DartdocReportSerializerMixin - implements ReportData { +class DartdocReport implements ReportData { @override String get reportType => ReportType.dartdoc; @override final String reportStatus; - @override final double coverageScore; - @override final List suggestions; DartdocReport({ @@ -362,4 +343,7 @@ class DartdocReport extends Object factory DartdocReport.fromJson(Map json) => _$DartdocReportFromJson(json); + + @override + Map toJson() => _$DartdocReportToJson(this); } diff --git a/app/lib/scorecard/models.g.dart b/app/lib/scorecard/models.g.dart index 1135664e33..3db79a2967 100644 --- a/app/lib/scorecard/models.g.dart +++ b/app/lib/scorecard/models.g.dart @@ -1,11 +1,5 @@ -// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file -// 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. - // GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: prefer_final_locals - part of 'models.dart'; // ************************************************************************** @@ -13,7 +7,7 @@ part of 'models.dart'; // ************************************************************************** ScoreCardData _$ScoreCardDataFromJson(Map json) { - return new ScoreCardData( + return ScoreCardData( packageName: json['packageName'] as String, packageVersion: json['packageVersion'] as String, runtimeVersion: json['runtimeVersion'] as String, @@ -36,37 +30,25 @@ ScoreCardData _$ScoreCardDataFromJson(Map json) { (json['reportTypes'] as List)?.map((e) => e as String)?.toList()); } -abstract class _$ScoreCardDataSerializerMixin { - String get packageName; - String get packageVersion; - String get runtimeVersion; - DateTime get updated; - DateTime get packageCreated; - DateTime get packageVersionCreated; - double get healthScore; - double get maintenanceScore; - double get popularityScore; - List get platformTags; - List get flags; - List get reportTypes; - Map toJson() => { - 'packageName': packageName, - 'packageVersion': packageVersion, - 'runtimeVersion': runtimeVersion, - 'updated': updated?.toIso8601String(), - 'packageCreated': packageCreated?.toIso8601String(), - 'packageVersionCreated': packageVersionCreated?.toIso8601String(), - 'healthScore': healthScore, - 'maintenanceScore': maintenanceScore, - 'popularityScore': popularityScore, - 'platformTags': platformTags, - 'flags': flags, - 'reportTypes': reportTypes - }; -} +Map _$ScoreCardDataToJson(ScoreCardData instance) => + { + 'packageName': instance.packageName, + 'packageVersion': instance.packageVersion, + 'runtimeVersion': instance.runtimeVersion, + 'updated': instance.updated?.toIso8601String(), + 'packageCreated': instance.packageCreated?.toIso8601String(), + 'packageVersionCreated': + instance.packageVersionCreated?.toIso8601String(), + 'healthScore': instance.healthScore, + 'maintenanceScore': instance.maintenanceScore, + 'popularityScore': instance.popularityScore, + 'platformTags': instance.platformTags, + 'flags': instance.flags, + 'reportTypes': instance.reportTypes + }; PanaReport _$PanaReportFromJson(Map json) { - return new PanaReport( + return PanaReport( reportStatus: json['reportStatus'] as String, healthScore: (json['healthScore'] as num)?.toDouble(), maintenanceScore: (json['maintenanceScore'] as num)?.toDouble(), @@ -76,52 +58,38 @@ PanaReport _$PanaReportFromJson(Map json) { pkgDependencies: (json['pkgDependencies'] as List) ?.map((e) => e == null ? null - : new PkgDependency.fromJson(e as Map)) + : PkgDependency.fromJson(e as Map)) ?.toList(), suggestions: (json['suggestions'] as List) - ?.map((e) => e == null - ? null - : new Suggestion.fromJson(e as Map)) + ?.map((e) => + e == null ? null : Suggestion.fromJson(e as Map)) ?.toList()); } -abstract class _$PanaReportSerializerMixin { - String get reportStatus; - double get healthScore; - double get maintenanceScore; - List get platformTags; - String get platformReason; - List get pkgDependencies; - List get suggestions; - Map toJson() => { - 'reportStatus': reportStatus, - 'healthScore': healthScore, - 'maintenanceScore': maintenanceScore, - 'platformTags': platformTags, - 'platformReason': platformReason, - 'pkgDependencies': pkgDependencies, - 'suggestions': suggestions - }; -} +Map _$PanaReportToJson(PanaReport instance) => + { + 'reportStatus': instance.reportStatus, + 'healthScore': instance.healthScore, + 'maintenanceScore': instance.maintenanceScore, + 'platformTags': instance.platformTags, + 'platformReason': instance.platformReason, + 'pkgDependencies': instance.pkgDependencies, + 'suggestions': instance.suggestions + }; DartdocReport _$DartdocReportFromJson(Map json) { - return new DartdocReport( + return DartdocReport( reportStatus: json['reportStatus'] as String, coverageScore: (json['coverageScore'] as num)?.toDouble(), suggestions: (json['suggestions'] as List) - ?.map((e) => e == null - ? null - : new Suggestion.fromJson(e as Map)) + ?.map((e) => + e == null ? null : Suggestion.fromJson(e as Map)) ?.toList()); } -abstract class _$DartdocReportSerializerMixin { - String get reportStatus; - double get coverageScore; - List get suggestions; - Map toJson() => { - 'reportStatus': reportStatus, - 'coverageScore': coverageScore, - 'suggestions': suggestions - }; -} +Map _$DartdocReportToJson(DartdocReport instance) => + { + 'reportStatus': instance.reportStatus, + 'coverageScore': instance.coverageScore, + 'suggestions': instance.suggestions + }; From 23d1de9544ba3376187652e3784267bf951653c3 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Tue, 4 Sep 2018 09:39:46 +0200 Subject: [PATCH 6/7] Using fold to get the latest version. --- app/lib/scorecard/backend.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/lib/scorecard/backend.dart b/app/lib/scorecard/backend.dart index 0ced167452..a077971438 100644 --- a/app/lib/scorecard/backend.dart +++ b/app/lib/scorecard/backend.dart @@ -69,9 +69,13 @@ class ScoreCardBackend { if (all.isEmpty) { return null; } - all.sort((a, b) => - isNewer(a.semanticRuntimeVersion, b.semanticRuntimeVersion) ? -1 : 1); - final data = all.last.toData(); + final latest = all.fold( + all.first, + (latest, current) => isNewer( + latest.semanticRuntimeVersion, current.semanticRuntimeVersion) + ? current + : latest); + final data = latest.toData(); await scoreCardMemcache.setScoreCardData(data); return data; } From 0c37605bd963a52568e114b6435424175adbe9ba Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Tue, 4 Sep 2018 10:43:44 +0200 Subject: [PATCH 7/7] More documentation. --- app/lib/scorecard/backend.dart | 11 +++++++++++ app/lib/scorecard/models.dart | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/app/lib/scorecard/backend.dart b/app/lib/scorecard/backend.dart index a077971438..fbb4fd9ce8 100644 --- a/app/lib/scorecard/backend.dart +++ b/app/lib/scorecard/backend.dart @@ -30,10 +30,15 @@ void registerScoreCardBackend(ScoreCardBackend backend) => ScoreCardBackend get scoreCardBackend => ss.lookup(#_scorecard_backend) as ScoreCardBackend; +/// Handles the data store and lookup for ScoreCard. class ScoreCardBackend { final db.DatastoreDB _db; ScoreCardBackend(this._db); + /// Returns the [ScoreCardData] for the given package and version. + /// + /// When [onlyCurrent] is false, it will try to load earlier versions of the + /// data. Future getScoreCardData( String packageName, String packageVersion, { @@ -80,6 +85,9 @@ class ScoreCardBackend { return data; } + /// Creates or updates a [ScoreCardReport] entry with the report's [data]. + /// The [data] will be converted to json and stored as a byte in the report + /// entry. Future updateReport( String packageName, String packageVersion, ReportData data) async { final key = scoreCardKey(packageName, packageVersion) @@ -109,6 +117,7 @@ class ScoreCardBackend { }); } + /// Load and deserialize the reports for the given package and version. Future> loadReports( String packageName, String packageVersion, {List reportTypes}) async { @@ -128,6 +137,8 @@ class ScoreCardBackend { return result; } + /// Updates the [ScoreCard] entry, reading both the package and version data, + /// alongside the data from reports, and compiles a new summary of them. Future updateScoreCard(String packageName, String packageVersion) async { final key = scoreCardKey(packageName, packageVersion); final pAndPv = await _db.lookup([key.parent, key.parent.parent]); diff --git a/app/lib/scorecard/models.dart b/app/lib/scorecard/models.dart index 6e98177642..cedf43ae15 100644 --- a/app/lib/scorecard/models.dart +++ b/app/lib/scorecard/models.dart @@ -43,6 +43,9 @@ abstract class ReportStatus { } /// Summary of various reports for a given PackageVersion. +/// +/// The details are pulled in from various data sources, and the entry is +/// recalculated from scratch each time any of the sources change. @db.Kind(name: 'ScoreCard', idType: db.IdType.String) class ScoreCard extends db.ExpandoModel { @db.StringProperty(required: true) @@ -80,6 +83,7 @@ class ScoreCard extends db.ExpandoModel { List platformTags; /// The flags for the package, version or analysis. + /// Example values: entries from [PackageFlags]. @CompatibleStringListProperty() List flags; @@ -300,6 +304,7 @@ class PanaReport implements ReportData { @CompatibleStringListProperty() List platformTags; + /// The reason pana decided on the [platformTags]. final String platformReason; final List pkgDependencies;