// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/worker_pool_mac.h"

#include "base/logging.h"
#include "base/mac/scoped_nsautorelease_pool.h"
#include "base/metrics/histogram.h"
#include "base/scoped_ptr.h"
#import "base/singleton_objc.h"
#include "base/task.h"
#include "base/third_party/dynamic_annotations/dynamic_annotations.h"
#include "base/worker_pool_linux.h"

// When C++ exceptions are disabled, the C++ library defines |try| and
// |catch| so as to allow exception-expecting C++ code to build properly when
// language support for exceptions is not present.  These macros interfere
// with the use of |@try| and |@catch| in Objective-C files such as this one.
// Undefine these macros here, after everything has been #included, since
// there will be no C++ uses and only Objective-C uses from this point on.
#undef try
#undef catch

namespace {

// |true| to use the Linux WorkerPool implementation for
// |WorkerPool::PostTask()|.
bool use_linux_workerpool_ = true;

Lock lock_;
base::Time last_check_;            // Last hung-test check.
std::vector<id> outstanding_ops_;  // Outstanding operations at last check.
size_t running_ = 0;               // Operations in |Run()|.
size_t outstanding_ = 0;           // Operations posted but not completed.

}  // namespace

namespace worker_pool_mac {

void SetUseLinuxWorkerPool(bool flag) {
  use_linux_workerpool_ = flag;
}

}  // namespace worker_pool_mac

@implementation WorkerPoolObjC

+ (NSOperationQueue*)sharedOperationQueue {
  return SingletonObjC<NSOperationQueue>::get();
}

@end  // @implementation WorkerPoolObjC

// TaskOperation adapts Task->Run() for use in an NSOperationQueue.
@interface TaskOperation : NSOperation {
 @private
  scoped_ptr<Task> task_;
}

// Returns an autoreleased instance of TaskOperation.  See -initWithTask: for
// details.
+ (id)taskOperationWithTask:(Task*)task;

// Designated initializer.  |task| is adopted as the Task* whose Run method
// this operation will call when executed.
- (id)initWithTask:(Task*)task;

@end  // @interface TaskOperation

@implementation TaskOperation

+ (id)taskOperationWithTask:(Task*)task {
  return [[[TaskOperation alloc] initWithTask:task] autorelease];
}

- (id)init {
  return [self initWithTask:NULL];
}

- (id)initWithTask:(Task*)task {
  if ((self = [super init])) {
    task_.reset(task);
  }
  return self;
}

- (void)main {
  DCHECK(task_.get()) << "-[TaskOperation main] called with no task";
  if (!task_.get()) {
    return;
  }

  {
    AutoLock locked(lock_);
    ++running_;
  }

  base::mac::ScopedNSAutoreleasePool autoreleasePool;

  @try {
    task_->Run();
  } @catch(NSException* exception) {
    LOG(ERROR) << "-[TaskOperation main] caught an NSException: "
               << [[exception description] UTF8String];
  } @catch(id exception) {
    LOG(ERROR) << "-[TaskOperation main] caught an unknown exception";
  }

  task_.reset(NULL);

  {
    AutoLock locked(lock_);
    --running_;
    --outstanding_;
  }
}

- (void)dealloc {
  // Getting the task_ contents without a lock can lead to a benign data race.
  // We annotate it to stay silent under ThreadSanitizer.
  ANNOTATE_IGNORE_READS_BEGIN();
  DCHECK(!task_.get())
      << "-[TaskOperation dealloc] called without running task";
  ANNOTATE_IGNORE_READS_END();
  [super dealloc];
}

@end  // @implementation TaskOperation

bool WorkerPool::PostTask(const tracked_objects::Location& from_here,
                          Task* task, bool task_is_slow) {
  if (use_linux_workerpool_) {
    return worker_pool_mac::MacPostTaskHelper(from_here, task, task_is_slow);
  }

  base::mac::ScopedNSAutoreleasePool autorelease_pool;

  // Ignore |task_is_slow|, it doesn't map directly to any tunable aspect of
  // an NSOperation.

  DCHECK(task) << "WorkerPool::PostTask called with no task";
  if (!task) {
    return false;
  }

  task->SetBirthPlace(from_here);

  NSOperationQueue* operation_queue = [WorkerPoolObjC sharedOperationQueue];
  [operation_queue addOperation:[TaskOperation taskOperationWithTask:task]];

  if ([operation_queue isSuspended]) {
    LOG(WARNING) << "WorkerPool::PostTask freeing stuck NSOperationQueue";

    // Nothing should ever be suspending this queue, but in case it winds up
    // happening, free things up.  This is a purely speculative shot in the
    // dark for http://crbug.com/20471.
    [operation_queue setSuspended:NO];
  }

  // Periodically calculate the set of operations which have not made
  // progress and report how many there are.  This should provide a
  // sense of how many clients are seeing hung operations of any sort,
  // and a sense of how many clients are seeing "too many" hung
  // operations.
  std::vector<id> hung_ops;
  size_t outstanding_delta = 0;
  size_t running_ops = 0;
  {
    const base::TimeDelta kCheckPeriod(base::TimeDelta::FromMinutes(10));
    base::Time now = base::Time::Now();

    AutoLock locked(lock_);
    ++outstanding_;
    running_ops = running_;
    if (last_check_.is_null() || now - last_check_ > kCheckPeriod) {
      base::mac::ScopedNSAutoreleasePool autoreleasePool;
      std::vector<id> ops;
      for (id op in [operation_queue operations]) {
        // DO NOT RETAIN.
        ops.push_back(op);
      }
      std::sort(ops.begin(), ops.end());

      outstanding_delta = outstanding_ - ops.size();

      std::set_intersection(outstanding_ops_.begin(), outstanding_ops_.end(),
                            ops.begin(), ops.end(),
                            std::back_inserter(hung_ops));

      outstanding_ops_.swap(ops);
      last_check_ = now;
    }
  }

  // Don't report "nothing to report".
  const size_t kUnaccountedOpsDelta = 10;
  if (hung_ops.size() > 0 || outstanding_delta > kUnaccountedOpsDelta) {
    UMA_HISTOGRAM_COUNTS_100("OSX.HungWorkers", hung_ops.size());
    UMA_HISTOGRAM_COUNTS_100("OSX.OutstandingDelta", outstanding_delta);
    UMA_HISTOGRAM_COUNTS_100("OSX.RunningOps", running_ops);
  }

  return true;
}