/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.builder.png; import com.android.annotations.NonNull; import com.android.builder.tasks.Job; import com.android.builder.tasks.JobContext; import com.android.builder.tasks.QueueThreadContext; import com.android.builder.tasks.Task; import com.android.builder.tasks.WorkQueue; import com.android.ide.common.internal.PngCruncher; import com.android.ide.common.internal.PngException; import com.android.utils.ILogger; import com.google.common.base.Objects; import java.io.File; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; /** * implementation of {@link com.android.ide.common.internal.PngCruncher} that queues request and * use a pool or aapt server processes to serve those. */ public class QueuedCruncher implements PngCruncher { // use an enum to ensure singleton. public enum Builder { INSTANCE; private final Map<String, QueuedCruncher> sInstances = new ConcurrentHashMap<String, QueuedCruncher>(); private final Object sLock = new Object(); /** * Creates a new {@link com.android.builder.png.QueuedCruncher} or return an existing one * based on the underlying AAPT executable location. * @param aaptLocation the APPT executable location. * @param logger the logger to use * @return a new of existing instance of the {@link com.android.builder.png.QueuedCruncher} */ public QueuedCruncher newCruncher( @NonNull String aaptLocation, @NonNull ILogger logger) { synchronized (sLock) { logger.info("QueuedCruncher is using %1$s", aaptLocation); if (!sInstances.containsKey(aaptLocation)) { QueuedCruncher queuedCruncher = new QueuedCruncher(aaptLocation, logger); sInstances.put(aaptLocation, queuedCruncher); } return sInstances.get(aaptLocation); } } } @NonNull private final String mAaptLocation; @NonNull private final ILogger mLogger; // Queue responsible for handling all passed jobs with a pool of worker threads. @NonNull private final WorkQueue<AaptProcess> mCrunchingRequests; // list of outstanding jobs. @NonNull private final Map<Integer, ConcurrentLinkedQueue<Job<AaptProcess>>> mOutstandingJobs = new ConcurrentHashMap<Integer, ConcurrentLinkedQueue<Job<AaptProcess>>>(); // ref count of active users, if it drops to zero, that means there are no more active users // and the queue should be shutdown. @NonNull private final AtomicInteger refCount = new AtomicInteger(0); // per process unique key provider to remember which users enlisted which requests. @NonNull private final AtomicInteger keyProvider = new AtomicInteger(0); private QueuedCruncher( @NonNull String aaptLocation, @NonNull ILogger iLogger) { mAaptLocation = aaptLocation; mLogger = iLogger; QueueThreadContext<AaptProcess> queueThreadContext = new QueueThreadContext<AaptProcess>() { // move this to a TLS, but do not store instances of AaptProcess in it. @NonNull private final Map<String, AaptProcess> mAaptProcesses = new ConcurrentHashMap<String, AaptProcess>(); @Override public void creation(@NonNull Thread t) throws IOException { try { mLogger.verbose("Thread(%1$s): create aapt slave", Thread.currentThread().getName()); AaptProcess aaptProcess = new AaptProcess.Builder(mAaptLocation, mLogger).start(); assert aaptProcess != null; aaptProcess.waitForReady(); mAaptProcesses.put(t.getName(), aaptProcess); } catch (InterruptedException e) { mLogger.error(e, "Cannot start slave process"); e.printStackTrace(); } } @Override public void runTask(@NonNull Job<AaptProcess> job) throws Exception { job.runTask( new JobContext<AaptProcess>( mAaptProcesses.get(Thread.currentThread().getName()))); mOutstandingJobs.get(((QueuedJob) job).key).remove(job); } @Override public void destruction(@NonNull Thread t) throws IOException, InterruptedException { AaptProcess aaptProcess = mAaptProcesses.get(Thread.currentThread().getName()); if (aaptProcess != null) { mLogger.verbose("Thread(%1$s): notify aapt slave shutdown", Thread.currentThread().getName()); aaptProcess.shutdown(); mAaptProcesses.remove(t.getName()); mLogger.verbose("Thread(%1$s): after shutdown queue_size=%2$d", Thread.currentThread().getName(), mAaptProcesses.size()); } } @Override public void shutdown() { if (!mAaptProcesses.isEmpty()) { mLogger.warning("Process list not empty"); for (Map.Entry<String, AaptProcess> aaptProcessEntry : mAaptProcesses .entrySet()) { mLogger.warning("Thread(%1$s): queue not cleaned", aaptProcessEntry.getKey()); try { aaptProcessEntry.getValue().shutdown(); } catch (Exception e) { mLogger.error(e, "while shutting down" + aaptProcessEntry.getKey()); } } } mAaptProcesses.clear(); } }; mCrunchingRequests = new WorkQueue<AaptProcess>( mLogger, queueThreadContext, "png-cruncher", 5, 2f); } private static final class QueuedJob extends Job<AaptProcess> { private final int key; public QueuedJob(int key, String jobTile, Task<AaptProcess> task) { super(jobTile, task); this.key = key; } } @Override public void crunchPng(int key, @NonNull final File from, @NonNull final File to) throws PngException { try { final Job<AaptProcess> aaptProcessJob = new QueuedJob( key, "Cruncher " + from.getName(), new Task<AaptProcess>() { @Override public void run(@NonNull Job<AaptProcess> job, @NonNull JobContext<AaptProcess> context) throws IOException { mLogger.verbose("Thread(%1$s): begin executing job %2$s", Thread.currentThread().getName(), job.getJobTitle()); context.getPayload().crunch(from, to, job); mLogger.verbose("Thread(%1$s): done executing job %2$s", Thread.currentThread().getName(), job.getJobTitle()); } @Override public String toString() { return Objects.toStringHelper(this) .add("from", from.getAbsolutePath()) .add("to", to.getAbsolutePath()) .toString(); } }); mOutstandingJobs.get(key).add(aaptProcessJob); mCrunchingRequests.push(aaptProcessJob); } catch (InterruptedException e) { // Restore the interrupted status Thread.currentThread().interrupt(); throw new PngException(e); } } private void waitForAll(int key) throws InterruptedException { mLogger.verbose("Thread(%1$s): begin waitForAll", Thread.currentThread().getName()); ConcurrentLinkedQueue<Job<AaptProcess>> jobs = mOutstandingJobs.get(key); Job<AaptProcess> aaptProcessJob = jobs.poll(); while (aaptProcessJob != null) { mLogger.verbose("Thread(%1$s) : wait for {%2$s)", Thread.currentThread().getName(), aaptProcessJob.toString()); if (!aaptProcessJob.await()) { throw new RuntimeException( "Crunching " + aaptProcessJob.getJobTitle() + " failed, see logs"); } aaptProcessJob = jobs.poll(); } mLogger.verbose("Thread(%1$s): end waitForAll", Thread.currentThread().getName()); } @Override public synchronized int start() { // increment our reference count. refCount.incrementAndGet(); // get a unique key for the lifetime of this process. int key = keyProvider.incrementAndGet(); mOutstandingJobs.put(key, new ConcurrentLinkedQueue<Job<AaptProcess>>()); return key; } @Override public synchronized void end(int key) throws InterruptedException { long startTime = System.currentTimeMillis(); try { waitForAll(key); mOutstandingJobs.get(key).clear(); mLogger.verbose("Job finished in %1$d", System.currentTimeMillis() - startTime); } finally { // even if we have failures, we need to shutdown property the sub processes. if (refCount.decrementAndGet() == 0) { mCrunchingRequests.shutdown(); mLogger.verbose("Shutdown finished in %1$d", System.currentTimeMillis() - startTime); } } } }