lib/ResourceScenario.rb in taskjuggler-0.0.7 vs lib/ResourceScenario.rb in taskjuggler-0.0.8
- old
+ new
@@ -35,19 +35,27 @@
# The scoreboard is only created when needed to save memory for projects
# which read-in the coporate employee database but only need a small
# subset.
@scoreboard = nil
+ # The index of the earliest booked time slot.
@firstBookedSlot = nil
+ # Same but for each assigned resource.
+ @firstBookedSlots = {}
+ # The index of the last booked time Slot.
@lastBookedSlot = nil
+ # Same but for each assigned resource.
+ @lastBookedSlots = {}
+
+ @dCache = DataCache.instance
end
# This method must be called at the beginning of each scheduling run. It
# initializes variables used during the scheduling process.
def prepareScheduling
@property['effort', @scenarioIdx] = 0
- initScoreboard
+ initScoreboard if @property.leaf?
setDirectReports
end
# The criticalness of a resource is a measure for the probabilty that all
@@ -89,11 +97,10 @@
a('managers').each do |r|
r.setReports_i(@scenarioIdx, [ @property ])
end
end
-
def preScheduleCheck
a('managers').each do |manager|
unless manager.leaf?
error('manager_is_group',
"Resource #{@property.fullId} has group #{manager.fullId} " +
@@ -104,10 +111,29 @@
"Resource #{@property.fullId} cannot manage itself.")
end
end
end
+ # This method does some housekeeping work after the scheduling is
+ # completed. It's meant to be called for top-level resources and then
+ # recursively descends into all child resources.
+ def finishScheduling
+ # Recursively descend into all child resources.
+ @property.children.each do |resource|
+ resource.finishScheduling(@scenarioIdx)
+ end
+
+ if (parent = @property.parent)
+ # Add the assigned task to the parent duties list.
+ a('duties').each do |task|
+ unless parent['duties', @scenarioIdx].include?(task)
+ parent['duties', @scenarioIdx] << task
+ end
+ end
+ end
+ end
+
def postScheduleCheck
if a('fail') || a('warn')
queryAttrs = { 'project' => @project,
'scenarioIdx' => @scenarioIdx,
'property' => @property,
@@ -156,10 +182,15 @@
# If +force+ is true, overwrite the existing booking for this slot. The
# method returns true if the slot was available.
def book(sbIdx, task, force = false)
return false if !force && !available?(sbIdx)
+ # Make sure the task is in the list of duties.
+ unless a('duties').include?(task)
+ @property['duties', @scenarioIdx] << task
+ end
+
#puts "Booking resource #{@property.fullId} at " +
# "#{@scoreboard.idxToDate(sbIdx)}/#{sbIdx} for task #{task.fullId}\n"
@scoreboard[sbIdx] = task
# Track the total allocated slots for this resource and all parent
# resources.
@@ -168,21 +199,28 @@
t['effort', @scenarioIdx] += 1
t = t.parent
end
a('limits').inc(@scoreboard.idxToDate(sbIdx)) if a('limits')
- # Make sure the task is in the list of duties.
- unless a('duties').include?(task)
- @property['duties', @scenarioIdx] << task
- end
-
+ # Scoreboard iterations are fairly expensive but they are very frequent
+ # operations in later processing. To limit the interations to the
+ # relevant intervals, we store the interval for all bookings and for
+ # each individual task.
if @firstBookedSlot.nil? || @firstBookedSlot > sbIdx
@firstBookedSlot = sbIdx
end
if @lastBookedSlot.nil? || @lastBookedSlot < sbIdx
@lastBookedSlot = sbIdx
end
+ if task
+ if @firstBookedSlots[task].nil? || @firstBookedSlots[task] > sbIdx
+ @firstBookedSlots[task] = sbIdx
+ end
+ if @lastBookedSlots[task].nil? || @lastBookedSlots[task] < sbIdx
+ @lastBookedSlots[task] = sbIdx
+ end
+ end
true
end
def bookBooking(sbIdx, booking)
initScoreboard if @scoreboard.nil?
@@ -289,10 +327,20 @@
end
# Returns the work of the resource (and its children) weighted by their
# efficiency.
def getEffectiveWork(startIdx, endIdx, task = nil)
+ # There can't be any effective work if the start is after the end or the
+ # todo list doesn't contain the specified task.
+ return 0.0 if startIdx >= endIdx || (task && !a('duties').include?(task))
+
+ # The unique key we use to address the result in the cache.
+ key = [ self, :ResourceScenarioEffectiveWork, startIdx, endIdx,
+ task ].hash
+ work = @dCache.load(key)
+ return work if work
+
# Convert the interval dates to indexes if needed.
startIdx = @project.dateToIdx(startIdx, true) if startIdx.is_a?(TjTime)
endIdx = @project.dateToIdx(endIdx, true) if endIdx.is_a?(TjTime)
work = 0.0
@@ -305,11 +353,11 @@
work = @project.convertToDailyLoad(
getAllocatedSlots(startIdx, endIdx, task) *
@project['scheduleGranularity']) * a('efficiency')
end
- work
+ @dCache.store(work, key)
end
# Returns the allocated accumulated time of this resource and its children.
def getAllocatedTime(startIdx, endIdx, task = nil)
# Convert the interval dates to indexes if needed.
@@ -417,20 +465,17 @@
# Returns true if the resource or any of its children is allocated during
# the period specified with the Interval _iv_. If task is not nil
# only allocations to this tasks are respected.
def allocated?(iv, task = nil)
- initScoreboard if @scoreboard.nil?
+ return false if task && !a('duties').include?(task)
- startIdx = @scoreboard.dateToIdx(iv.start, true)
- endIdx = @scoreboard.dateToIdx(iv.end, true)
+ startIdx = @project.dateToIdx(iv.start, true)
+ endIdx = @project.dateToIdx(iv.end, true)
- startIdx = @firstBookedSlot if @firstBookedSlot &&
- startIdx < @firstBookedSlot
- endIdx = @lastBookedSlot + 1 if @lastBookedSlot &&
- endIdx < @lastBookedSlot + 1
- return false if startIdx > endIdx
+ startIdx, endIdx = fitIndicies(startIdx, endIdx, task)
+ return false if startIdx >= endIdx
return allocatedSub(startIdx, endIdx, task)
end
# Iterate over the scoreboard and turn its content into a set of Bookings.
@@ -459,12 +504,10 @@
# If we don't have a Booking for the task yet, we create one.
if bookings[lastTask].nil?
bookings[lastTask] = Booking.new(@property, lastTask, [])
end
- # Make sure the index is correct even for the last task block.
- idx += 1 if idx == endIdx
# Append the new interval to the Booking.
bookings[lastTask].intervals <<
Interval.new(@scoreboard.idxToDate(bookingStart),
@scoreboard.idxToDate(idx))
end
@@ -492,33 +535,32 @@
end
# Count the booked slots between the start and end index. If _task_ is not
# nil count only those slots that are assigned to this particular task.
def getAllocatedSlots(startIdx, endIdx, task)
- # To speedup the counting we start with the first booked slot and end
- # with the last booked slot.
- startIdx = @firstBookedSlot if @firstBookedSlot &&
- startIdx < @firstBookedSlot
- endIdx = @lastBookedSlot + 1 if @lastBookedSlot &&
- endIdx > @lastBookedSlot + 1
+ # If there is no scoreboard, we don't have any allocations.
+ return 0 unless @scoreboard
- initScoreboard if @scoreboard.nil?
+ startIdx, endIdx = fitIndicies(startIdx, endIdx, task)
+ return 0 if startIdx >= endIdx
+
bookedSlots = 0
- startIdx.upto(endIdx - 1) do |idx|
- if (task.nil? && @scoreboard[idx].is_a?(Task)) ||
- (task && @scoreboard[idx] == task)
+ @scoreboard.each(startIdx, endIdx) do |slot|
+ if (task.nil? && slot.is_a?(Task)) || (task && slot == task)
bookedSlots += 1
end
end
bookedSlots
end
# Count the free slots between the start and end index.
def getFreeSlots(startIdx, endIdx)
- initScoreboard if @scoreboard.nil?
+ return 0 if startIdx >= endIdx
+ initScoreboard unless @scoreboard
+
freeSlots = 0
startIdx.upto(endIdx - 1) do |idx|
freeSlots += 1 if @scoreboard[idx].nil?
end
@@ -526,12 +568,14 @@
end
# Count the regular work time slots between the start and end index that
# have been blocked by a vacation.
def getVacationSlots(startIdx, endIdx)
- initScoreboard if @scoreboard.nil?
+ return 0 if startIdx >= endIdx
+ initScoreboard unless @scoreboard
+
vacationSlots = 0
startIdx.upto(endIdx - 1) do |idx|
val = @scoreboard[idx]
# Bit 1 needs to be unset and the vacation bits must not be 0.
vacationSlots += 1 if val.is_a?(Fixnum) && (val & 0x2) == 0 &&
@@ -601,10 +645,28 @@
end
end
end
end
+ # Limit the _startIdx_ and _endIdx_ to the actually assigned interval.
+ # If _task_ is provided, fit it for the bookings of this particular task.
+ def fitIndicies(startIdx, endIdx, task = nil)
+ if task
+ startIdx = @firstBookedSlots[task] if @firstBookedSlots[task] &&
+ startIdx < @firstBookedSlots[task]
+ endIdx = @lastBookedSlots[task] + 1 if @lastBookedSlots[task] &&
+ endIdx >
+ @lastBookedSlots[task] + 1
+ else
+ startIdx = @firstBookedSlot if @firstBookedSlot &&
+ startIdx < @firstBookedSlot
+ endIdx = @lastBookedSlot + 1 if @lastBookedSlot &&
+ endIdx > @lastBookedSlot + 1
+ end
+ [ startIdx, endIdx ]
+ end
+
def setReports_i(reports)
if reports.include?(@property)
# A manager must never show up in the list of his/her own reports.
error('manager_loop',
"Management loop detected. #{@property.fullId} has self " +
@@ -636,10 +698,13 @@
@property.children.each do |resource|
return true if resource.allocatedSub(@scenarioIdx, startIdx, endIdx,
task)
end
else
- return false unless a('duties').include?(task)
+ return false unless @scoreboard && a('duties').include?(task)
+
+ startIdx, endIdx = fitIndicies(startIdx, endIdx, task)
+ return false if startIdx >= endIdx
startIdx.upto(endIdx - 1) do |idx|
return true if @scoreboard[idx] == task
end
end