Home | History | Annotate | Download | only in surfacecomposition
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.surfacecomposition;
     17 
     18 import java.text.DecimalFormat;
     19 import java.util.ArrayList;
     20 import java.util.List;
     21 
     22 import android.app.ActionBar;
     23 import android.app.Activity;
     24 import android.app.ActivityManager;
     25 import android.app.ActivityManager.MemoryInfo;
     26 import android.content.Context;
     27 import android.content.pm.PackageManager;
     28 import android.graphics.Color;
     29 import android.graphics.PixelFormat;
     30 import android.graphics.Rect;
     31 import android.graphics.drawable.ColorDrawable;
     32 import android.os.Bundle;
     33 import android.view.Display;
     34 import android.view.View;
     35 import android.view.View.OnClickListener;
     36 import android.view.ViewGroup;
     37 import android.view.Window;
     38 import android.view.WindowManager;
     39 import android.widget.ArrayAdapter;
     40 import android.widget.Button;
     41 import android.widget.LinearLayout;
     42 import android.widget.RelativeLayout;
     43 import android.widget.Spinner;
     44 import android.widget.TextView;
     45 
     46 /**
     47  * This activity is designed to measure peformance scores of Android surfaces.
     48  * It can work in two modes. In first mode functionality of this activity is
     49  * invoked from Cts test (SurfaceCompositionTest). This activity can also be
     50  * used in manual mode as a normal app. Different pixel formats are supported.
     51  *
     52  * measureCompositionScore(pixelFormat)
     53  *   This test measures surface compositor performance which shows how many
     54  *   surfaces of specific format surface compositor can combine without dropping
     55  *   frames. We allow one dropped frame per half second.
     56  *
     57  * measureAllocationScore(pixelFormat)
     58  *   This test measures surface allocation/deallocation performance. It shows
     59  *   how many surface lifecycles (creation, destruction) can be done per second.
     60  *
     61  * In manual mode, which activated by pressing button 'Compositor speed' or
     62  * 'Allocator speed', all possible pixel format are tested and combined result
     63  * is displayed in text view. Additional system information such as memory
     64  * status, display size and surface format is also displayed and regulary
     65  * updated.
     66  */
     67 public class SurfaceCompositionMeasuringActivity extends Activity implements OnClickListener {
     68     private final static int MIN_NUMBER_OF_SURFACES = 15;
     69     private final static int MAX_NUMBER_OF_SURFACES = 40;
     70     private final static int WARM_UP_ALLOCATION_CYCLES = 2;
     71     private final static int MEASURE_ALLOCATION_CYCLES = 5;
     72     private final static int TEST_COMPOSITOR = 1;
     73     private final static int TEST_ALLOCATION = 2;
     74     private final static float MIN_REFRESH_RATE_SUPPORTED = 50.0f;
     75 
     76     private final static DecimalFormat DOUBLE_FORMAT = new DecimalFormat("#.00");
     77     // Possible selection in pixel format selector.
     78     private final static int[] PIXEL_FORMATS = new int[] {
     79             PixelFormat.TRANSLUCENT,
     80             PixelFormat.TRANSPARENT,
     81             PixelFormat.OPAQUE,
     82             PixelFormat.RGBA_8888,
     83             PixelFormat.RGBX_8888,
     84             PixelFormat.RGB_888,
     85             PixelFormat.RGB_565,
     86     };
     87 
     88 
     89     private List<CustomSurfaceView> mViews = new ArrayList<CustomSurfaceView>();
     90     private Button mMeasureCompositionButton;
     91     private Button mMeasureAllocationButton;
     92     private Spinner mPixelFormatSelector;
     93     private TextView mResultView;
     94     private TextView mSystemInfoView;
     95     private final Object mLockResumed = new Object();
     96     private boolean mResumed;
     97 
     98     // Drop one frame per half second.
     99     private double mRefreshRate;
    100     private double mTargetFPS;
    101     private boolean mAndromeda;
    102 
    103     private int mWidth;
    104     private int mHeight;
    105 
    106     class CompositorScore {
    107         double mSurfaces;
    108         double mBandwidth;
    109 
    110         @Override
    111         public String toString() {
    112             return DOUBLE_FORMAT.format(mSurfaces) + " surfaces. " +
    113                     "Bandwidth: " + getReadableMemory((long)mBandwidth) + "/s";
    114         }
    115     }
    116 
    117     /**
    118      * Measure performance score.
    119      *
    120      * @return biggest possible number of visible surfaces which surface
    121      *         compositor can handle.
    122      */
    123     public CompositorScore measureCompositionScore(int pixelFormat) {
    124         waitForActivityResumed();
    125         //MemoryAccessTask memAccessTask = new MemoryAccessTask();
    126         //memAccessTask.start();
    127         // Destroy any active surface.
    128         configureSurfacesAndWait(0, pixelFormat, false);
    129         CompositorScore score = new CompositorScore();
    130         score.mSurfaces = measureCompositionScore(new Measurement(0, 60.0),
    131                 new Measurement(mViews.size() + 1, 0.0f), pixelFormat);
    132         // Assume 32 bits per pixel.
    133         score.mBandwidth = score.mSurfaces * mTargetFPS * mWidth * mHeight * 4.0;
    134         //memAccessTask.stop();
    135         return score;
    136     }
    137 
    138     static class AllocationScore {
    139         double mMedian;
    140         double mMin;
    141         double mMax;
    142 
    143         @Override
    144         public String toString() {
    145             return DOUBLE_FORMAT.format(mMedian) + " (min:" + DOUBLE_FORMAT.format(mMin) +
    146                     ", max:" + DOUBLE_FORMAT.format(mMax) + ") surface allocations per second";
    147         }
    148     }
    149 
    150     public AllocationScore measureAllocationScore(int pixelFormat) {
    151         waitForActivityResumed();
    152         AllocationScore score = new AllocationScore();
    153         for (int i = 0; i < MEASURE_ALLOCATION_CYCLES + WARM_UP_ALLOCATION_CYCLES; ++i) {
    154             long time1 = System.currentTimeMillis();
    155             configureSurfacesAndWait(MIN_NUMBER_OF_SURFACES, pixelFormat, false);
    156             acquireSurfacesCanvas();
    157             long time2 = System.currentTimeMillis();
    158             releaseSurfacesCanvas();
    159             configureSurfacesAndWait(0, pixelFormat, false);
    160             // Give SurfaceFlinger some time to rebuild the layer stack and release the buffers.
    161             try {
    162                 Thread.sleep(500);
    163             } catch(InterruptedException e) {
    164                 e.printStackTrace();
    165             }
    166             if (i < WARM_UP_ALLOCATION_CYCLES) {
    167                 // This is warm-up cycles, ignore result so far.
    168                 continue;
    169             }
    170             double speed = MIN_NUMBER_OF_SURFACES * 1000.0 / (time2 - time1);
    171             score.mMedian += speed / MEASURE_ALLOCATION_CYCLES;
    172             if (i == WARM_UP_ALLOCATION_CYCLES) {
    173                 score.mMin = speed;
    174                 score.mMax = speed;
    175             } else {
    176                 score.mMin = Math.min(score.mMin, speed);
    177                 score.mMax = Math.max(score.mMax, speed);
    178             }
    179         }
    180 
    181         return score;
    182     }
    183 
    184     public boolean isAndromeda() {
    185         return mAndromeda;
    186     }
    187 
    188     @Override
    189     public void onClick(View view) {
    190         if (view == mMeasureCompositionButton) {
    191             doTest(TEST_COMPOSITOR);
    192         } else if (view == mMeasureAllocationButton) {
    193             doTest(TEST_ALLOCATION);
    194         }
    195     }
    196 
    197     private void doTest(final int test) {
    198         enableControls(false);
    199         final int pixelFormat = PIXEL_FORMATS[mPixelFormatSelector.getSelectedItemPosition()];
    200         new Thread() {
    201             public void run() {
    202                 final StringBuffer sb = new StringBuffer();
    203                 switch (test) {
    204                     case TEST_COMPOSITOR: {
    205                             sb.append("Compositor score:");
    206                             CompositorScore score = measureCompositionScore(pixelFormat);
    207                             sb.append("\n    " + getPixelFormatInfo(pixelFormat) + ":" +
    208                                     score + ".");
    209                         }
    210                         break;
    211                     case TEST_ALLOCATION: {
    212                             sb.append("Allocation score:");
    213                             AllocationScore score = measureAllocationScore(pixelFormat);
    214                             sb.append("\n    " + getPixelFormatInfo(pixelFormat) + ":" +
    215                                     score + ".");
    216                         }
    217                         break;
    218                 }
    219                 runOnUiThreadAndWait(new Runnable() {
    220                     public void run() {
    221                         mResultView.setText(sb.toString());
    222                         enableControls(true);
    223                         updateSystemInfo(pixelFormat);
    224                     }
    225                 });
    226             }
    227         }.start();
    228     }
    229 
    230     /**
    231      * Wait until activity is resumed.
    232      */
    233     public void waitForActivityResumed() {
    234         synchronized (mLockResumed) {
    235             if (!mResumed) {
    236                 try {
    237                     mLockResumed.wait(10000);
    238                 } catch (InterruptedException e) {
    239                 }
    240             }
    241             if (!mResumed) {
    242                 throw new RuntimeException("Activity was not resumed");
    243             }
    244         }
    245     }
    246 
    247     @Override
    248     protected void onCreate(Bundle savedInstanceState) {
    249         super.onCreate(savedInstanceState);
    250 
    251         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    252 
    253         // Detect Andromeda devices by having free-form window management feature.
    254         mAndromeda = getPackageManager().hasSystemFeature(
    255                 PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT);
    256         detectRefreshRate();
    257 
    258         // To layouts in parent. First contains list of Surfaces and second
    259         // controls. Controls stay on top.
    260         RelativeLayout rootLayout = new RelativeLayout(this);
    261         rootLayout.setLayoutParams(new ViewGroup.LayoutParams(
    262                 ViewGroup.LayoutParams.MATCH_PARENT,
    263                 ViewGroup.LayoutParams.MATCH_PARENT));
    264 
    265         CustomLayout layout = new CustomLayout(this);
    266         layout.setLayoutParams(new ViewGroup.LayoutParams(
    267                 ViewGroup.LayoutParams.MATCH_PARENT,
    268                 ViewGroup.LayoutParams.MATCH_PARENT));
    269 
    270         Rect rect = new Rect();
    271         getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
    272         mWidth = rect.right;
    273         mHeight = rect.bottom;
    274         long maxMemoryPerSurface = roundToNextPowerOf2(mWidth) * roundToNextPowerOf2(mHeight) * 4;
    275         // Use 75% of available memory.
    276         int surfaceCnt = (int)((getMemoryInfo().availMem * 3) / (4 * maxMemoryPerSurface));
    277         if (surfaceCnt < MIN_NUMBER_OF_SURFACES) {
    278             throw new RuntimeException("Not enough memory to allocate " +
    279                     MIN_NUMBER_OF_SURFACES + " surfaces.");
    280         }
    281         if (surfaceCnt > MAX_NUMBER_OF_SURFACES) {
    282             surfaceCnt = MAX_NUMBER_OF_SURFACES;
    283         }
    284 
    285         LinearLayout controlLayout = new LinearLayout(this);
    286         controlLayout.setOrientation(LinearLayout.VERTICAL);
    287         controlLayout.setLayoutParams(new ViewGroup.LayoutParams(
    288                 ViewGroup.LayoutParams.MATCH_PARENT,
    289                 ViewGroup.LayoutParams.MATCH_PARENT));
    290 
    291         mMeasureCompositionButton = createButton("Compositor speed.", controlLayout);
    292         mMeasureAllocationButton = createButton("Allocation speed", controlLayout);
    293 
    294         String[] pixelFomats = new String[PIXEL_FORMATS.length];
    295         for (int i = 0; i < pixelFomats.length; ++i) {
    296             pixelFomats[i] = getPixelFormatInfo(PIXEL_FORMATS[i]);
    297         }
    298         mPixelFormatSelector = new Spinner(this);
    299         ArrayAdapter<String> pixelFormatSelectorAdapter =
    300                 new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, pixelFomats);
    301         pixelFormatSelectorAdapter.setDropDownViewResource(
    302                 android.R.layout.simple_spinner_dropdown_item);
    303         mPixelFormatSelector.setAdapter(pixelFormatSelectorAdapter);
    304         mPixelFormatSelector.setLayoutParams(new LinearLayout.LayoutParams(
    305                 ViewGroup.LayoutParams.WRAP_CONTENT,
    306                 ViewGroup.LayoutParams.WRAP_CONTENT));
    307         controlLayout.addView(mPixelFormatSelector);
    308 
    309         mResultView = new TextView(this);
    310         mResultView.setBackgroundColor(0);
    311         mResultView.setText("Press button to start test.");
    312         mResultView.setLayoutParams(new LinearLayout.LayoutParams(
    313                 ViewGroup.LayoutParams.WRAP_CONTENT,
    314                 ViewGroup.LayoutParams.WRAP_CONTENT));
    315         controlLayout.addView(mResultView);
    316 
    317         mSystemInfoView = new TextView(this);
    318         mSystemInfoView.setBackgroundColor(0);
    319         mSystemInfoView.setLayoutParams(new LinearLayout.LayoutParams(
    320                 ViewGroup.LayoutParams.WRAP_CONTENT,
    321                 ViewGroup.LayoutParams.WRAP_CONTENT));
    322         controlLayout.addView(mSystemInfoView);
    323 
    324         for (int i = 0; i < surfaceCnt; ++i) {
    325             CustomSurfaceView view = new CustomSurfaceView(this, "Surface:" + i);
    326             // Create all surfaces overlapped in order to prevent SurfaceFlinger
    327             // to filter out surfaces by optimization in case surface is opaque.
    328             // In case surface is transparent it will be drawn anyway. Note that first
    329             // surface covers whole screen and must stand below other surfaces. Z order of
    330             // layers is not predictable and there is only one way to force first
    331             // layer to be below others is to mark it as media and all other layers
    332             // to mark as media overlay.
    333             if (i == 0) {
    334                 view.setLayoutParams(new CustomLayout.LayoutParams(0, 0, mWidth, mHeight));
    335                 view.setZOrderMediaOverlay(false);
    336             } else {
    337                 // Z order of other layers is not predefined so make offset on x and reverse
    338                 // offset on y to make sure that surface is visible in any layout.
    339                 int x = i;
    340                 int y = (surfaceCnt - i);
    341                 view.setLayoutParams(new CustomLayout.LayoutParams(x, y, x + mWidth, y + mHeight));
    342                 view.setZOrderMediaOverlay(true);
    343             }
    344             view.setVisibility(View.INVISIBLE);
    345             layout.addView(view);
    346             mViews.add(view);
    347         }
    348 
    349         rootLayout.addView(layout);
    350         rootLayout.addView(controlLayout);
    351 
    352         setContentView(rootLayout);
    353     }
    354 
    355     private Button createButton(String caption, LinearLayout layout) {
    356         Button button = new Button(this);
    357         button.setText(caption);
    358         button.setLayoutParams(new LinearLayout.LayoutParams(
    359                 ViewGroup.LayoutParams.WRAP_CONTENT,
    360                 ViewGroup.LayoutParams.WRAP_CONTENT));
    361         button.setOnClickListener(this);
    362         layout.addView(button);
    363         return button;
    364     }
    365 
    366     private void enableControls(boolean enabled) {
    367         mMeasureCompositionButton.setEnabled(enabled);
    368         mMeasureAllocationButton.setEnabled(enabled);
    369         mPixelFormatSelector.setEnabled(enabled);
    370     }
    371 
    372     @Override
    373     protected void onResume() {
    374         super.onResume();
    375 
    376         updateSystemInfo(PixelFormat.UNKNOWN);
    377 
    378         synchronized (mLockResumed) {
    379             mResumed = true;
    380             mLockResumed.notifyAll();
    381         }
    382     }
    383 
    384     @Override
    385     protected void onPause() {
    386         super.onPause();
    387 
    388         synchronized (mLockResumed) {
    389             mResumed = false;
    390         }
    391     }
    392 
    393     class Measurement {
    394         Measurement(int surfaceCnt, double fps) {
    395             mSurfaceCnt = surfaceCnt;
    396             mFPS = fps;
    397         }
    398 
    399         public final int mSurfaceCnt;
    400         public final double mFPS;
    401     }
    402 
    403     private double measureCompositionScore(Measurement ok, Measurement fail, int pixelFormat) {
    404         if (ok.mSurfaceCnt + 1 == fail.mSurfaceCnt) {
    405             // Interpolate result.
    406             double fraction = (mTargetFPS - fail.mFPS) / (ok.mFPS - fail.mFPS);
    407             return ok.mSurfaceCnt + fraction;
    408         }
    409 
    410         int medianSurfaceCnt = (ok.mSurfaceCnt + fail.mSurfaceCnt) / 2;
    411         Measurement median = new Measurement(medianSurfaceCnt,
    412                 measureFPS(medianSurfaceCnt, pixelFormat));
    413 
    414         if (median.mFPS >= mTargetFPS) {
    415             return measureCompositionScore(median, fail, pixelFormat);
    416         } else {
    417             return measureCompositionScore(ok, median, pixelFormat);
    418         }
    419     }
    420 
    421     private double measureFPS(int surfaceCnt, int pixelFormat) {
    422         configureSurfacesAndWait(surfaceCnt, pixelFormat, true);
    423         // At least one view is visible and it is enough to update only
    424         // one overlapped surface in order to force SurfaceFlinger to send
    425         // all surfaces to compositor.
    426         double fps = mViews.get(0).measureFPS(mRefreshRate * 0.8, mRefreshRate * 0.999);
    427 
    428         // Make sure that surface configuration was not changed.
    429         validateSurfacesNotChanged();
    430 
    431         return fps;
    432     }
    433 
    434     private void waitForSurfacesConfigured(final int pixelFormat) {
    435         for (int i = 0; i < mViews.size(); ++i) {
    436             CustomSurfaceView view = mViews.get(i);
    437             if (view.getVisibility() == View.VISIBLE) {
    438                 view.waitForSurfaceReady();
    439             } else {
    440                 view.waitForSurfaceDestroyed();
    441             }
    442         }
    443         runOnUiThreadAndWait(new Runnable() {
    444             @Override
    445             public void run() {
    446                 updateSystemInfo(pixelFormat);
    447             }
    448         });
    449     }
    450 
    451     private void validateSurfacesNotChanged() {
    452         for (int i = 0; i < mViews.size(); ++i) {
    453             CustomSurfaceView view = mViews.get(i);
    454             view.validateSurfaceNotChanged();
    455         }
    456     }
    457 
    458     private void configureSurfaces(int surfaceCnt, int pixelFormat, boolean invalidate) {
    459         for (int i = 0; i < mViews.size(); ++i) {
    460             CustomSurfaceView view = mViews.get(i);
    461             if (i < surfaceCnt) {
    462                 view.setMode(pixelFormat, invalidate);
    463                 view.setVisibility(View.VISIBLE);
    464             } else {
    465                 view.setVisibility(View.INVISIBLE);
    466             }
    467         }
    468     }
    469 
    470     private void configureSurfacesAndWait(final int surfaceCnt, final int pixelFormat,
    471             final boolean invalidate) {
    472         runOnUiThreadAndWait(new Runnable() {
    473             @Override
    474             public void run() {
    475                 configureSurfaces(surfaceCnt, pixelFormat, invalidate);
    476             }
    477         });
    478         waitForSurfacesConfigured(pixelFormat);
    479     }
    480 
    481     private void acquireSurfacesCanvas() {
    482         for (int i = 0; i < mViews.size(); ++i) {
    483             CustomSurfaceView view = mViews.get(i);
    484             view.acquireCanvas();
    485         }
    486     }
    487 
    488     private void releaseSurfacesCanvas() {
    489         for (int i = 0; i < mViews.size(); ++i) {
    490             CustomSurfaceView view = mViews.get(i);
    491             view.releaseCanvas();
    492         }
    493     }
    494 
    495     private static String getReadableMemory(long bytes) {
    496         long unit = 1024;
    497         if (bytes < unit) {
    498             return bytes + " B";
    499         }
    500         int exp = (int) (Math.log(bytes) / Math.log(unit));
    501         return String.format("%.1f %sB", bytes / Math.pow(unit, exp),
    502                 "KMGTPE".charAt(exp-1));
    503     }
    504 
    505     private MemoryInfo getMemoryInfo() {
    506         ActivityManager activityManager = (ActivityManager)
    507                 getSystemService(ACTIVITY_SERVICE);
    508         MemoryInfo memInfo = new MemoryInfo();
    509         activityManager.getMemoryInfo(memInfo);
    510         return memInfo;
    511     }
    512 
    513     private void updateSystemInfo(int pixelFormat) {
    514         int visibleCnt = 0;
    515         for (int i = 0; i < mViews.size(); ++i) {
    516             if (mViews.get(i).getVisibility() == View.VISIBLE) {
    517                 ++visibleCnt;
    518             }
    519         }
    520 
    521         MemoryInfo memInfo = getMemoryInfo();
    522         String platformName = mAndromeda ? "Andromeda" : "Android";
    523         String info = platformName + ": available " +
    524                 getReadableMemory(memInfo.availMem) + " from " +
    525                 getReadableMemory(memInfo.totalMem) + ".\nVisible " +
    526                 visibleCnt + " from " + mViews.size() + " " +
    527                 getPixelFormatInfo(pixelFormat) + " surfaces.\n" +
    528                 "View size: " + mWidth + "x" + mHeight +
    529                 ". Refresh rate: " + DOUBLE_FORMAT.format(mRefreshRate) + ".";
    530         mSystemInfoView.setText(info);
    531     }
    532 
    533     private void detectRefreshRate() {
    534         WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
    535         mRefreshRate = wm.getDefaultDisplay().getRefreshRate();
    536         if (mRefreshRate < MIN_REFRESH_RATE_SUPPORTED)
    537             throw new RuntimeException("Unsupported display refresh rate: " + mRefreshRate);
    538         mTargetFPS = mRefreshRate - 2.0f;
    539     }
    540 
    541     private int roundToNextPowerOf2(int value) {
    542         --value;
    543         value |= value >> 1;
    544         value |= value >> 2;
    545         value |= value >> 4;
    546         value |= value >> 8;
    547         value |= value >> 16;
    548         return value + 1;
    549     }
    550 
    551     public static String getPixelFormatInfo(int pixelFormat) {
    552         switch (pixelFormat) {
    553         case PixelFormat.TRANSLUCENT:
    554             return "TRANSLUCENT";
    555         case PixelFormat.TRANSPARENT:
    556             return "TRANSPARENT";
    557         case PixelFormat.OPAQUE:
    558             return "OPAQUE";
    559         case PixelFormat.RGBA_8888:
    560             return "RGBA_8888";
    561         case PixelFormat.RGBX_8888:
    562             return "RGBX_8888";
    563         case PixelFormat.RGB_888:
    564             return "RGB_888";
    565         case PixelFormat.RGB_565:
    566             return "RGB_565";
    567         default:
    568             return "PIX.FORMAT:" + pixelFormat;
    569         }
    570     }
    571 
    572     /**
    573      * A helper that executes a task in the UI thread and waits for its completion.
    574      *
    575      * @param task - task to execute.
    576      */
    577     private void runOnUiThreadAndWait(Runnable task) {
    578         new UIExecutor(task);
    579     }
    580 
    581     class UIExecutor implements Runnable {
    582         private final Object mLock = new Object();
    583         private Runnable mTask;
    584         private boolean mDone = false;
    585 
    586         UIExecutor(Runnable task) {
    587             mTask = task;
    588             mDone = false;
    589             runOnUiThread(this);
    590             synchronized (mLock) {
    591                 while (!mDone) {
    592                     try {
    593                         mLock.wait();
    594                     } catch (InterruptedException e) {
    595                         e.printStackTrace();
    596                     }
    597                 }
    598             }
    599         }
    600 
    601         public void run() {
    602             mTask.run();
    603             synchronized (mLock) {
    604                 mDone = true;
    605                 mLock.notify();
    606             }
    607         }
    608     }
    609 }
    610