[SPARK-3795] Heuristics for dynamically scaling executors

This is part of a bigger effort to provide elastic scaling of executors within a Spark application ([SPARK-3174](https://issues.apache.org/jira/browse/SPARK-3174)). This PR does not provide any functionality by itself; it is a skeleton that is missing a mechanism to be added later in [SPARK-3822](https://issues.apache.org/jira/browse/SPARK-3822).

Comments and feedback are most welcome. For those of you reviewing this in detail, I highly recommend doing it through your favorite IDE instead of through the diff here.
Rather than accepting the time all over the place, it's more common to implement a pluggable clock like this:
```
trait Clock {
  def getTime(): Long
}

class RealClock extends Clock {
  override def getTime() = System.currentTimeMillis
}

class TestClock extends Clock {
  var curTime: Long = 0
  override def getTime() = curTime
  def tick(ms: Long) = curTime += tick
}

private[spark] class ExecutorAllocationManager(sc: SparkContext, clock: Clock = new RealClock()) extends Logging {
```