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