ちょっと話題の記事

[速報]機械学習でコードレビュー!? Amazon CodeGuru がついに GA されました!

2020.06.30

re:Invent2019 で発表されていた機械学習でコードレビューをしてくれるサービス、Amazon CodeGuru がついに GA されました!

Amazon CodeGuru とは

Amazon CodeGuru は、自動化されたコードレビューとアプリケーションパフォーマンスの推奨事項を提供する機械学習サービスです。アプリケーションのパフォーマンスを損なう最もコストがかかるコード行を見つけ、一晩中トラブルシューティングを続けるのに役立ちます。さらにコードを修正または改善するための具体的な推奨事項を示してくれます。

CodeGuru によるコードレビューはルールマイニングと、ロジスティック回帰とニューラルネットワークの組み合わせを活用した教師あり機械学習モデルを使用して行われます。CodeGuru の機械学習モデルには、Amazon 内部での数十万のプロジェクトと 10000 を超える Github の OSS プロジェクトをベースにトレーニングされており、さらに何万人の Amazon 開発者が Codeguru のトレーニングに貢献してきました。

CodeGuru に推奨された項目を受け入れるかどうかは開発者が判断します。フィードバックを多く与えるほど CodeGuru の推奨事項が向上します。

参考:Amazon CodeGuru

使いかた

CodeGuru Reviewer

CodeGuru Reviewerにレビューしたいコードを格納しているレポジトリを紐付けます。 対応しているレポジトリは以下の三つです。

  • GitHub
  • AWS CodeCommit
  • Bitbucket

CodeGuru Profiler

レポジトリ内のソースコードをレビューするのがAmazon CodeGuru Profilerです。

Amazon CodeGuru Profiler は実際にプロダクションで動いているコードのパフォーマンスをランタイムで常にレビューし、推奨項目、例えば CPU の消費を抑えられる箇所や必要以上に処理に時間がかかっている箇所を提供します。コードレビューの結果はダッシュボードに表示されます。実際のユーザートラフィックを元にしたパフォーマンスの分析を行うため、CodeGuru の利用はプロダクション環境で行うことが推奨されています。

CodeGuru ダッシュボードでは以下の問題の解決に役立つ項目が可視化されます。

  • CPU のレイテンシや使用率のトラブルシュート
  • インフラストラクチャコスト削減
  • パフォーマンス向上

CodeGuru Profiler が現在サポートするのは Java と Java virtual machine (JVM)で動くアプリケーションのソースコードです。

利用できる Region

以下の 10 リージョンで利用可能です。

  • US East (N. Virginia)
  • US East (Ohio)
  • US West (Oregon)
  • EU (Ireland)
  • EU (London)
  • EU (Frankfurt)
  • EU (Stockholm)
  • Asia Pacific (Singapore)
  • Asia Pacific (Sydney)
  • Asia Pacific (Tokyo)

料金

Amazon CodeGuru の利用料は分析されるコード 100 行あたり 0.75USD/月です。 利用した分だけ課金される Pay Per Use 形式で課金されます。

まずは90 日間の無料トライアルがあるのでまずは試してみるのがいいでしょう。

やってみた

こちらのチュートリアルをやってみました!手順は以下の通りです。

  • Step1: チュートリアル用のレポジトリをフォーク
  • Step2: CodeGuru にレポジトリを紐付ける
  • Step3: コードを書き換えてレポジトリへ Push してみる
  • Step4: プルリクを作成してみる
  • Step5: CodeGuru によるレビュー結果を確認

Step1: チュートリアル用のレポジトリをフォーク

https://github.com/aws-samples/amazon-codeguru-reviewer-sample-appを自分の Github アカウントにフォークします。

Step2: CodeGuru にレポジトリを紐付ける

マネジメントコンソールから CodeGuru Reviewer とレポジトリの紐付けを行います。レポジトリの種類は Github を選択します。

Github アカウントへ AWS からのアクセスを承認します。 これでレポジトリを選択できるようになるので、先ほどフォークしたサンプルレポジトリを選択します。

ここまででレポジトリの紐付けが完了しました。

Step3: コードを書き換えてレポジトリへ Push する

先ほどフォークしたレポジトリをローカル環境へクローンします。

git clone https://github.com/USER_ID/amazon-codeguru-reviewer-sample-app.git

新しくブランチを切って、サンプルの Java コードに書き換えます。

cd amazon-codeguru-reviewer-sample-app
git switch -c develop
cp src/main/java/com/shipmentEvents/handlers/EventHandler.java src/main/java/com/shipmentEvents/demo/

EventHandler.javaが新しく作成されたので Push しましょう。

git add --all
git commit -m 'new demo file'
git push --set-upstream origin dev

Step4: プルリクを作成してみる

早速プルリクエストを作成してみましょう。作成されたプルリクエストの内容を CodeGuru がレビューしてくれます。

コンソールのコードレビューをみるとレビュー中になっていることが確認できました。完了するまでしばし待ちます・・・。

Step5: CodeGuru によるレビュー結果を確認

マネジメントコンソールから CodeGuru のダッシュボードを開きます。

レビューされている・・・!

レビュー内容は先ほどのプルリクエストから確認できます。

コードレビューの内容を確認

最後にレビューの内容をみてみましょう。

コードの概要

Shipment events for a carrier are uploaded to separate S3 buckets based on the source of events. E.g., events originating from the hand-held scanner are stored in a separate bucket than the ones from mobile App. The Lambda processes events from multiple sources and updates the latest status of the package in a summary S3 bucket every 15 minutes.

要約:

配送物のトラッキングデータを処理するLambdaハンドラです。配送物が出荷されたことを知らせるイベントはハンドスキャナーやモバイルアプリなどから取得されますが、イベントの発生源に応じてそれぞれのS3バケットへ情報は格納されます。例えば、ハンドスキャナーからのデータをトリガに発生したイベントデータは、モバイルアプリから発生したイベントデータとは別のバケットに格納されます。Lambdaは複数のソースからのイベントを処理し、15分ごとにパッケージの最新のステータスをサマリーS3バケットに更新します。

プルリクエストに含まれるサンプルコードの全文がこちらです。

package com.shipmentEvents.handlers;

import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.amazonaws.regions.Regions;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.events.ScheduledEvent;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.DeleteObjectsRequest;
import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.shipmentEvents.util.Constants;

import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;


public class EventHandler implements RequestHandler<ScheduledEvent, String> {

    /**
     * Shipment events for a carrier are uploaded to separate S3 buckets based on the source of events. E.g., events originating from
     * the hand-held scanner are stored in a separate bucket than the ones from mobile App. The Lambda processes events from multiple
     * sources and updates the latest status of the package in a summary S3 bucket every 15 minutes.
     *
     * The events are stored in following format:
     * - Each status update is a file, where the name of the file is tracking number + random id.
     * - Each file has status and time-stamp as the first 2 lines respectively.
     * - The time at which the file is stored in S3 is not an indication of the time-stamp of the event.
     * - Once the status is marked as DELIVERED, we can stop tracking the package.
     *
     * A Sample files looks as below:
     *  FILE-NAME-> '8787323232232332--55322798-dd29-4a04-97f4-93e18feed554'
     *   >status:IN TRANSIT
     *   >timestamp: 1573410202
     *   >Other fields like...tracking history and address
     */
    public String handleRequest(ScheduledEvent scheduledEvent, Context context) {

        final LambdaLogger logger = context.getLogger();
        try {
            processShipmentUpdates(logger);
            return "SUCCESS";
        } catch (final Exception ex) {
            logger.log(String.format("Failed to process shipment Updates in %s due to %s", scheduledEvent.getAccount(), ex.getMessage()));
            throw new RuntimeException(ex);
        }
    }


    private void processShipmentUpdates(final LambdaLogger logger) throws InterruptedException {

        final List<String> bucketsToProcess = Constants.BUCKETS_TO_PROCESS;
        final Map<String, Pair<Long, String>> latestStatusForTrackingNumber = new HashMap<String, Pair<Long, String>>();
        final Map<String, List<KeyVersion>> filesToDelete = new HashMap<String, List<DeleteObjectsRequest.KeyVersion>>();
        for (final String bucketName : bucketsToProcess) {
            final List<KeyVersion> filesProcessed = processEventsInBucket(bucketName, logger, latestStatusForTrackingNumber);
            filesToDelete.put(bucketName, filesProcessed);
        }
        final AmazonS3 s3Client = EventHandler.getS3Client();

        //Create a new file in the Constants.SUMMARY_BUCKET
        logger.log("Map of statuses -> " + latestStatusForTrackingNumber);
        String summaryUpdateName = Long.toString(System.currentTimeMillis());

        EventHandler.getS3Client().putObject(Constants.SUMMARY_BUCKET, summaryUpdateName, latestStatusForTrackingNumber.toString());

        long expirationTime = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
        while(System.currentTimeMillis() < expirationTime) {
            if (s3Client.doesObjectExist(Constants.SUMMARY_BUCKET, summaryUpdateName)) {
                break;
            }
            logger.log("waiting for file to be created " + summaryUpdateName);
            Thread.sleep(1000);
        }

        // Before we delete the shipment updates make sure the summary update file exists
        if (EventHandler.getS3Client().doesObjectExist(Constants.SUMMARY_BUCKET, summaryUpdateName)) {
            deleteProcessedFiles(filesToDelete);
            logger.log("All updates successfully processed");
        } else {
            throw new RuntimeException("Failed to write summary status, will be retried in 15 minutes");
        }

    }

    private List<KeyVersion> processEventsInBucket(String bucketName, LambdaLogger logger, Map<String, Pair<Long, String>> latestStatusForTrackingNumber) {
        final AmazonS3 s3Client = EventHandler.getS3Client();
        logger.log("Processing Bucket: " + bucketName);

        ObjectListing files = s3Client.listObjects(bucketName);
        List<KeyVersion> filesProcessed = new ArrayList<DeleteObjectsRequest.KeyVersion>();

        for (Iterator<?> iterator = files.getObjectSummaries().iterator(); iterator.hasNext(); ) {
            S3ObjectSummary summary = (S3ObjectSummary) iterator.next();
            logger.log("Reading Object: " + summary.getKey());

            String trackingNumber = summary.getKey().split("--")[0];
            Pair<Long, String> lastKnownStatus = latestStatusForTrackingNumber.get(trackingNumber);

            // Check if this shipment has already been delivered, skip this file
            if (lastKnownStatus != null && "DELIVERED".equals(lastKnownStatus.getRight())) {
                continue;
            }

            String fileContents = s3Client.getObjectAsString(bucketName, summary.getKey());

            if (!isValidFile(fileContents)) {
                logger.log(String.format("Skipping invalid file %s", summary.getKey()));
                continue;
            }

            if (!fileContents.contains("\n")) {

            }
            String[] lines = fileContents.split("\n");
            String line1 = lines[0];
            String line2 = lines[1];

            String status = line1.split(":")[1];
            Long timeStamp = Long.parseLong(line2.split(":")[1]);


            if (null == lastKnownStatus || lastKnownStatus.getLeft() < timeStamp) {
                lastKnownStatus = new MutablePair<Long, String>(timeStamp, status);
                latestStatusForTrackingNumber.put(trackingNumber, lastKnownStatus);
            }

            //Add to list of processed files
            filesProcessed.add(new KeyVersion(summary.getKey()));
            logger.log("logging Contents of the file" + fileContents);
        }
        return filesProcessed;
    }


    private void deleteProcessedFiles(Map<String, List<KeyVersion>> filesToDelete) {
      final AmazonS3 s3Client = EventHandler.getS3Client();
      for (Entry<String, List<KeyVersion>> entry : filesToDelete.entrySet()) {
          final DeleteObjectsRequest deleteRequest = new DeleteObjectsRequest(entry.getKey()).withKeys(entry.getValue()).withQuiet(false);
          s3Client.deleteObjects(deleteRequest);
      }
    }

    private boolean isValidFile(String fileContents) {
        if (!fileContents.contains("\n")) {
            return false;
        }
        String[] lines = fileContents.split("\n");
        for (String l: lines) {
            if (!l.contains(":")) {
                return false;
            }
        }
        return true;
    }

    public static AmazonS3 getS3Client() {
        return AmazonS3ClientBuilder.standard().withRegion(Regions.DEFAULT_REGION).build();
    }


}

コメント 1: #76~#79

long expirationTime = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
        while(System.currentTimeMillis() < expirationTime) {
            if (s3Client.doesObjectExist(Constants.SUMMARY_BUCKET, summaryUpdateName)) {

This code appears to be waiting for a resource before it runs. You could use the waiters feature to help improve efficiency. Consider using ObjectExists or ObjectNotExists. For more information, see https://aws.amazon.com/blogs/developer/waiters-in-the-aws-sdk-for-java/

訳: このコードは必要な期間よりも長くつぎの処理を待っているように見えます。処理の効率化を測るためにSDKのWaiter機能を利用することを推奨します。

コメント 2: #97~#100

final AmazonS3 s3Client = EventHandler.getS3Client();
        logger.log("Processing Bucket: " + bucketName);

        ObjectListing files = s3Client.listObjects(bucketName);

This code uses an outdated API. ListObjectsV2 is the revised List Objects API, and we recommend you use this revised API for new application developments.

訳: s3Client.listObjectsAPI には新しいバージョンのAPI、ListObjectsV2があるのでそっちを使うことを推奨します。

コメント 3: #97~#100

final AmazonS3 s3Client = EventHandler.getS3Client();
        logger.log("Processing Bucket: " + bucketName);

        ObjectListing files = s3Client.listObjects(bucketName);

This code might not produce accurate results if the operation returns paginated results instead of all results. Consider adding another call to check for additional results.

訳: このコードは取得したデータがページ分割されている場合、意図した動きをしない可能性があります。返却されたデータが全てのデータなのか、分割されたものなのかを確認する処理を追加することを推奨します。

コメント 4:#165~#168

public static AmazonS3 getS3Client() {
        return AmazonS3ClientBuilder.standard().withRegion(Regions.DEFAULT_REGION).build();

This code is written so that the client cannot be reused across invocations of the Lambda function. To improve the performance of the Lambda function, consider using static initialization/constructor, global/static variables and singletons. It allows to keep alive and reuse HTTP connections that were established during a previous invocation. Learn more about best practices for working with AWS Lambda functions.

訳: このコードは、S3クライアントが複数のLambda関数をまたいで再利用されないように書かれています。 Lambda関数のパフォーマンスを向上させるために、静的な初期化/コンストラクタ、グローバル/静的変数、シングルトンの使用を検討してください。これにより、以前の呼び出し時に確立された HTTP 接続を継続したまま再利用することができます。 AWS Lambda関数を利用するためのベストプラクティスについてはこちらをご覧ください。

コードレビューをレビューする

CodeGuruによるレビューの内容に対して、Like/Dislike絵文字をつけることでフィードバックを送信できます。 このフィードバックをもとにCodeGuru Reviewerは改善されます。

References