##// END OF EJS Templates
progress: add a lock to prepare for introducing a thread
Solomon Matthews -
r23908:5502bd79 default
parent child Browse files
Show More
@@ -1,317 +1,320 b''
1 # progress.py show progress bars for some actions
1 # progress.py show progress bars for some actions
2 #
2 #
3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """show progress bars for some actions
8 """show progress bars for some actions
9
9
10 This extension uses the progress information logged by hg commands
10 This extension uses the progress information logged by hg commands
11 to draw progress bars that are as informative as possible. Some progress
11 to draw progress bars that are as informative as possible. Some progress
12 bars only offer indeterminate information, while others have a definite
12 bars only offer indeterminate information, while others have a definite
13 end point.
13 end point.
14
14
15 The following settings are available::
15 The following settings are available::
16
16
17 [progress]
17 [progress]
18 delay = 3 # number of seconds (float) before showing the progress bar
18 delay = 3 # number of seconds (float) before showing the progress bar
19 changedelay = 1 # changedelay: minimum delay before showing a new topic.
19 changedelay = 1 # changedelay: minimum delay before showing a new topic.
20 # If set to less than 3 * refresh, that value will
20 # If set to less than 3 * refresh, that value will
21 # be used instead.
21 # be used instead.
22 refresh = 0.1 # time in seconds between refreshes of the progress bar
22 refresh = 0.1 # time in seconds between refreshes of the progress bar
23 format = topic bar number estimate # format of the progress bar
23 format = topic bar number estimate # format of the progress bar
24 width = <none> # if set, the maximum width of the progress information
24 width = <none> # if set, the maximum width of the progress information
25 # (that is, min(width, term width) will be used)
25 # (that is, min(width, term width) will be used)
26 clear-complete = True # clear the progress bar after it's done
26 clear-complete = True # clear the progress bar after it's done
27 disable = False # if true, don't show a progress bar
27 disable = False # if true, don't show a progress bar
28 assume-tty = False # if true, ALWAYS show a progress bar, unless
28 assume-tty = False # if true, ALWAYS show a progress bar, unless
29 # disable is given
29 # disable is given
30
30
31 Valid entries for the format field are topic, bar, number, unit,
31 Valid entries for the format field are topic, bar, number, unit,
32 estimate, speed, and item. item defaults to the last 20 characters of
32 estimate, speed, and item. item defaults to the last 20 characters of
33 the item, but this can be changed by adding either ``-<num>`` which
33 the item, but this can be changed by adding either ``-<num>`` which
34 would take the last num characters, or ``+<num>`` for the first num
34 would take the last num characters, or ``+<num>`` for the first num
35 characters.
35 characters.
36 """
36 """
37
37
38 import sys
38 import sys
39 import time
39 import time
40 import threading
40
41
41 from mercurial.i18n import _
42 from mercurial.i18n import _
42 testedwith = 'internal'
43 testedwith = 'internal'
43
44
44 from mercurial import encoding
45 from mercurial import encoding
45
46
46 def spacejoin(*args):
47 def spacejoin(*args):
47 return ' '.join(s for s in args if s)
48 return ' '.join(s for s in args if s)
48
49
49 def shouldprint(ui):
50 def shouldprint(ui):
50 return not ui.plain() and (ui._isatty(sys.stderr) or
51 return not ui.plain() and (ui._isatty(sys.stderr) or
51 ui.configbool('progress', 'assume-tty'))
52 ui.configbool('progress', 'assume-tty'))
52
53
53 def fmtremaining(seconds):
54 def fmtremaining(seconds):
54 if seconds < 60:
55 if seconds < 60:
55 # i18n: format XX seconds as "XXs"
56 # i18n: format XX seconds as "XXs"
56 return _("%02ds") % (seconds)
57 return _("%02ds") % (seconds)
57 minutes = seconds // 60
58 minutes = seconds // 60
58 if minutes < 60:
59 if minutes < 60:
59 seconds -= minutes * 60
60 seconds -= minutes * 60
60 # i18n: format X minutes and YY seconds as "XmYYs"
61 # i18n: format X minutes and YY seconds as "XmYYs"
61 return _("%dm%02ds") % (minutes, seconds)
62 return _("%dm%02ds") % (minutes, seconds)
62 # we're going to ignore seconds in this case
63 # we're going to ignore seconds in this case
63 minutes += 1
64 minutes += 1
64 hours = minutes // 60
65 hours = minutes // 60
65 minutes -= hours * 60
66 minutes -= hours * 60
66 if hours < 30:
67 if hours < 30:
67 # i18n: format X hours and YY minutes as "XhYYm"
68 # i18n: format X hours and YY minutes as "XhYYm"
68 return _("%dh%02dm") % (hours, minutes)
69 return _("%dh%02dm") % (hours, minutes)
69 # we're going to ignore minutes in this case
70 # we're going to ignore minutes in this case
70 hours += 1
71 hours += 1
71 days = hours // 24
72 days = hours // 24
72 hours -= days * 24
73 hours -= days * 24
73 if days < 15:
74 if days < 15:
74 # i18n: format X days and YY hours as "XdYYh"
75 # i18n: format X days and YY hours as "XdYYh"
75 return _("%dd%02dh") % (days, hours)
76 return _("%dd%02dh") % (days, hours)
76 # we're going to ignore hours in this case
77 # we're going to ignore hours in this case
77 days += 1
78 days += 1
78 weeks = days // 7
79 weeks = days // 7
79 days -= weeks * 7
80 days -= weeks * 7
80 if weeks < 55:
81 if weeks < 55:
81 # i18n: format X weeks and YY days as "XwYYd"
82 # i18n: format X weeks and YY days as "XwYYd"
82 return _("%dw%02dd") % (weeks, days)
83 return _("%dw%02dd") % (weeks, days)
83 # we're going to ignore days and treat a year as 52 weeks
84 # we're going to ignore days and treat a year as 52 weeks
84 weeks += 1
85 weeks += 1
85 years = weeks // 52
86 years = weeks // 52
86 weeks -= years * 52
87 weeks -= years * 52
87 # i18n: format X years and YY weeks as "XyYYw"
88 # i18n: format X years and YY weeks as "XyYYw"
88 return _("%dy%02dw") % (years, weeks)
89 return _("%dy%02dw") % (years, weeks)
89
90
90 class progbar(object):
91 class progbar(object):
91 def __init__(self, ui):
92 def __init__(self, ui):
92 self.ui = ui
93 self.ui = ui
94 self._refreshlock = threading.Lock()
93 self.resetstate()
95 self.resetstate()
94
96
95 def resetstate(self):
97 def resetstate(self):
96 self.topics = []
98 self.topics = []
97 self.topicstates = {}
99 self.topicstates = {}
98 self.starttimes = {}
100 self.starttimes = {}
99 self.startvals = {}
101 self.startvals = {}
100 self.printed = False
102 self.printed = False
101 self.lastprint = time.time() + float(self.ui.config(
103 self.lastprint = time.time() + float(self.ui.config(
102 'progress', 'delay', default=3))
104 'progress', 'delay', default=3))
103 self.curtopic = None
105 self.curtopic = None
104 self.lasttopic = None
106 self.lasttopic = None
105 self.indetcount = 0
107 self.indetcount = 0
106 self.refresh = float(self.ui.config(
108 self.refresh = float(self.ui.config(
107 'progress', 'refresh', default=0.1))
109 'progress', 'refresh', default=0.1))
108 self.changedelay = max(3 * self.refresh,
110 self.changedelay = max(3 * self.refresh,
109 float(self.ui.config(
111 float(self.ui.config(
110 'progress', 'changedelay', default=1)))
112 'progress', 'changedelay', default=1)))
111 self.order = self.ui.configlist(
113 self.order = self.ui.configlist(
112 'progress', 'format',
114 'progress', 'format',
113 default=['topic', 'bar', 'number', 'estimate'])
115 default=['topic', 'bar', 'number', 'estimate'])
114
116
115 def show(self, now, topic, pos, item, unit, total):
117 def show(self, now, topic, pos, item, unit, total):
116 if not shouldprint(self.ui):
118 if not shouldprint(self.ui):
117 return
119 return
118 termwidth = self.width()
120 termwidth = self.width()
119 self.printed = True
121 self.printed = True
120 head = ''
122 head = ''
121 needprogress = False
123 needprogress = False
122 tail = ''
124 tail = ''
123 for indicator in self.order:
125 for indicator in self.order:
124 add = ''
126 add = ''
125 if indicator == 'topic':
127 if indicator == 'topic':
126 add = topic
128 add = topic
127 elif indicator == 'number':
129 elif indicator == 'number':
128 if total:
130 if total:
129 add = ('% ' + str(len(str(total))) +
131 add = ('% ' + str(len(str(total))) +
130 's/%s') % (pos, total)
132 's/%s') % (pos, total)
131 else:
133 else:
132 add = str(pos)
134 add = str(pos)
133 elif indicator.startswith('item') and item:
135 elif indicator.startswith('item') and item:
134 slice = 'end'
136 slice = 'end'
135 if '-' in indicator:
137 if '-' in indicator:
136 wid = int(indicator.split('-')[1])
138 wid = int(indicator.split('-')[1])
137 elif '+' in indicator:
139 elif '+' in indicator:
138 slice = 'beginning'
140 slice = 'beginning'
139 wid = int(indicator.split('+')[1])
141 wid = int(indicator.split('+')[1])
140 else:
142 else:
141 wid = 20
143 wid = 20
142 if slice == 'end':
144 if slice == 'end':
143 add = encoding.trim(item, wid, leftside=True)
145 add = encoding.trim(item, wid, leftside=True)
144 else:
146 else:
145 add = encoding.trim(item, wid)
147 add = encoding.trim(item, wid)
146 add += (wid - encoding.colwidth(add)) * ' '
148 add += (wid - encoding.colwidth(add)) * ' '
147 elif indicator == 'bar':
149 elif indicator == 'bar':
148 add = ''
150 add = ''
149 needprogress = True
151 needprogress = True
150 elif indicator == 'unit' and unit:
152 elif indicator == 'unit' and unit:
151 add = unit
153 add = unit
152 elif indicator == 'estimate':
154 elif indicator == 'estimate':
153 add = self.estimate(topic, pos, total, now)
155 add = self.estimate(topic, pos, total, now)
154 elif indicator == 'speed':
156 elif indicator == 'speed':
155 add = self.speed(topic, pos, unit, now)
157 add = self.speed(topic, pos, unit, now)
156 if not needprogress:
158 if not needprogress:
157 head = spacejoin(head, add)
159 head = spacejoin(head, add)
158 else:
160 else:
159 tail = spacejoin(tail, add)
161 tail = spacejoin(tail, add)
160 if needprogress:
162 if needprogress:
161 used = 0
163 used = 0
162 if head:
164 if head:
163 used += encoding.colwidth(head) + 1
165 used += encoding.colwidth(head) + 1
164 if tail:
166 if tail:
165 used += encoding.colwidth(tail) + 1
167 used += encoding.colwidth(tail) + 1
166 progwidth = termwidth - used - 3
168 progwidth = termwidth - used - 3
167 if total and pos <= total:
169 if total and pos <= total:
168 amt = pos * progwidth // total
170 amt = pos * progwidth // total
169 bar = '=' * (amt - 1)
171 bar = '=' * (amt - 1)
170 if amt > 0:
172 if amt > 0:
171 bar += '>'
173 bar += '>'
172 bar += ' ' * (progwidth - amt)
174 bar += ' ' * (progwidth - amt)
173 else:
175 else:
174 progwidth -= 3
176 progwidth -= 3
175 self.indetcount += 1
177 self.indetcount += 1
176 # mod the count by twice the width so we can make the
178 # mod the count by twice the width so we can make the
177 # cursor bounce between the right and left sides
179 # cursor bounce between the right and left sides
178 amt = self.indetcount % (2 * progwidth)
180 amt = self.indetcount % (2 * progwidth)
179 amt -= progwidth
181 amt -= progwidth
180 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
182 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
181 ' ' * int(abs(amt)))
183 ' ' * int(abs(amt)))
182 prog = ''.join(('[', bar , ']'))
184 prog = ''.join(('[', bar , ']'))
183 out = spacejoin(head, prog, tail)
185 out = spacejoin(head, prog, tail)
184 else:
186 else:
185 out = spacejoin(head, tail)
187 out = spacejoin(head, tail)
186 sys.stderr.write('\r' + encoding.trim(out, termwidth))
188 sys.stderr.write('\r' + encoding.trim(out, termwidth))
187 self.lasttopic = topic
189 self.lasttopic = topic
188 sys.stderr.flush()
190 sys.stderr.flush()
189
191
190 def clear(self):
192 def clear(self):
191 if not shouldprint(self.ui):
193 if not shouldprint(self.ui):
192 return
194 return
193 sys.stderr.write('\r%s\r' % (' ' * self.width()))
195 sys.stderr.write('\r%s\r' % (' ' * self.width()))
194
196
195 def complete(self):
197 def complete(self):
196 if not shouldprint(self.ui):
198 if not shouldprint(self.ui):
197 return
199 return
198 if self.ui.configbool('progress', 'clear-complete', default=True):
200 if self.ui.configbool('progress', 'clear-complete', default=True):
199 self.clear()
201 self.clear()
200 else:
202 else:
201 sys.stderr.write('\n')
203 sys.stderr.write('\n')
202 sys.stderr.flush()
204 sys.stderr.flush()
203
205
204 def width(self):
206 def width(self):
205 tw = self.ui.termwidth()
207 tw = self.ui.termwidth()
206 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
208 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
207
209
208 def estimate(self, topic, pos, total, now):
210 def estimate(self, topic, pos, total, now):
209 if total is None:
211 if total is None:
210 return ''
212 return ''
211 initialpos = self.startvals[topic]
213 initialpos = self.startvals[topic]
212 target = total - initialpos
214 target = total - initialpos
213 delta = pos - initialpos
215 delta = pos - initialpos
214 if delta > 0:
216 if delta > 0:
215 elapsed = now - self.starttimes[topic]
217 elapsed = now - self.starttimes[topic]
216 if elapsed > float(
218 if elapsed > float(
217 self.ui.config('progress', 'estimate', default=2)):
219 self.ui.config('progress', 'estimate', default=2)):
218 seconds = (elapsed * (target - delta)) // delta + 1
220 seconds = (elapsed * (target - delta)) // delta + 1
219 return fmtremaining(seconds)
221 return fmtremaining(seconds)
220 return ''
222 return ''
221
223
222 def speed(self, topic, pos, unit, now):
224 def speed(self, topic, pos, unit, now):
223 initialpos = self.startvals[topic]
225 initialpos = self.startvals[topic]
224 delta = pos - initialpos
226 delta = pos - initialpos
225 elapsed = now - self.starttimes[topic]
227 elapsed = now - self.starttimes[topic]
226 if elapsed > float(
228 if elapsed > float(
227 self.ui.config('progress', 'estimate', default=2)):
229 self.ui.config('progress', 'estimate', default=2)):
228 return _('%d %s/sec') % (delta / elapsed, unit)
230 return _('%d %s/sec') % (delta / elapsed, unit)
229 return ''
231 return ''
230
232
231 def _oktoprint(self, now):
233 def _oktoprint(self, now):
232 '''Check if conditions are met to print - e.g. changedelay elapsed'''
234 '''Check if conditions are met to print - e.g. changedelay elapsed'''
233 if (self.lasttopic is None # first time we printed
235 if (self.lasttopic is None # first time we printed
234 # not a topic change
236 # not a topic change
235 or self.curtopic == self.lasttopic
237 or self.curtopic == self.lasttopic
236 # it's been long enough we should print anyway
238 # it's been long enough we should print anyway
237 or now - self.lastprint >= self.changedelay):
239 or now - self.lastprint >= self.changedelay):
238 return True
240 return True
239 else:
241 else:
240 return False
242 return False
241
243
242 def progress(self, topic, pos, item='', unit='', total=None):
244 def progress(self, topic, pos, item='', unit='', total=None):
243 now = time.time()
245 now = time.time()
246 self._refreshlock.acquire()
244 try:
247 try:
245 if pos is None:
248 if pos is None:
246 self.starttimes.pop(topic, None)
249 self.starttimes.pop(topic, None)
247 self.startvals.pop(topic, None)
250 self.startvals.pop(topic, None)
248 self.topicstates.pop(topic, None)
251 self.topicstates.pop(topic, None)
249 # reset the progress bar if this is the outermost topic
252 # reset the progress bar if this is the outermost topic
250 if self.topics and self.topics[0] == topic and self.printed:
253 if self.topics and self.topics[0] == topic and self.printed:
251 self.complete()
254 self.complete()
252 self.resetstate()
255 self.resetstate()
253 # truncate the list of topics assuming all topics within
256 # truncate the list of topics assuming all topics within
254 # this one are also closed
257 # this one are also closed
255 if topic in self.topics:
258 if topic in self.topics:
256 self.topics = self.topics[:self.topics.index(topic)]
259 self.topics = self.topics[:self.topics.index(topic)]
257 # reset the last topic to the one we just unwound to,
260 # reset the last topic to the one we just unwound to,
258 # so that higher-level topics will be stickier than
261 # so that higher-level topics will be stickier than
259 # lower-level topics
262 # lower-level topics
260 if self.topics:
263 if self.topics:
261 self.lasttopic = self.topics[-1]
264 self.lasttopic = self.topics[-1]
262 else:
265 else:
263 self.lasttopic = None
266 self.lasttopic = None
264 else:
267 else:
265 if topic not in self.topics:
268 if topic not in self.topics:
266 self.starttimes[topic] = now
269 self.starttimes[topic] = now
267 self.startvals[topic] = pos
270 self.startvals[topic] = pos
268 self.topics.append(topic)
271 self.topics.append(topic)
269 self.topicstates[topic] = pos, item, unit, total
272 self.topicstates[topic] = pos, item, unit, total
270 self.curtopic = topic
273 self.curtopic = topic
271 if now - self.lastprint >= self.refresh and self.topics:
274 if now - self.lastprint >= self.refresh and self.topics:
272 if self._oktoprint(now):
275 if self._oktoprint(now):
273 self.lastprint = now
276 self.lastprint = now
274 self.show(now, topic, *self.topicstates[topic])
277 self.show(now, topic, *self.topicstates[topic])
275 finally:
278 finally:
276 pass
279 self._refreshlock.release()
277
280
278 _singleton = None
281 _singleton = None
279
282
280 def uisetup(ui):
283 def uisetup(ui):
281 global _singleton
284 global _singleton
282 class progressui(ui.__class__):
285 class progressui(ui.__class__):
283 _progbar = None
286 _progbar = None
284
287
285 def _quiet(self):
288 def _quiet(self):
286 return self.debugflag or self.quiet
289 return self.debugflag or self.quiet
287
290
288 def progress(self, *args, **opts):
291 def progress(self, *args, **opts):
289 if not self._quiet():
292 if not self._quiet():
290 self._progbar.progress(*args, **opts)
293 self._progbar.progress(*args, **opts)
291 return super(progressui, self).progress(*args, **opts)
294 return super(progressui, self).progress(*args, **opts)
292
295
293 def write(self, *args, **opts):
296 def write(self, *args, **opts):
294 if not self._quiet() and self._progbar.printed:
297 if not self._quiet() and self._progbar.printed:
295 self._progbar.clear()
298 self._progbar.clear()
296 return super(progressui, self).write(*args, **opts)
299 return super(progressui, self).write(*args, **opts)
297
300
298 def write_err(self, *args, **opts):
301 def write_err(self, *args, **opts):
299 if not self._quiet() and self._progbar.printed:
302 if not self._quiet() and self._progbar.printed:
300 self._progbar.clear()
303 self._progbar.clear()
301 return super(progressui, self).write_err(*args, **opts)
304 return super(progressui, self).write_err(*args, **opts)
302
305
303 # Apps that derive a class from ui.ui() can use
306 # Apps that derive a class from ui.ui() can use
304 # setconfig('progress', 'disable', 'True') to disable this extension
307 # setconfig('progress', 'disable', 'True') to disable this extension
305 if ui.configbool('progress', 'disable'):
308 if ui.configbool('progress', 'disable'):
306 return
309 return
307 if shouldprint(ui) and not ui.debugflag and not ui.quiet:
310 if shouldprint(ui) and not ui.debugflag and not ui.quiet:
308 ui.__class__ = progressui
311 ui.__class__ = progressui
309 # we instantiate one globally shared progress bar to avoid
312 # we instantiate one globally shared progress bar to avoid
310 # competing progress bars when multiple UI objects get created
313 # competing progress bars when multiple UI objects get created
311 if not progressui._progbar:
314 if not progressui._progbar:
312 if _singleton is None:
315 if _singleton is None:
313 _singleton = progbar(ui)
316 _singleton = progbar(ui)
314 progressui._progbar = _singleton
317 progressui._progbar = _singleton
315
318
316 def reposetup(ui, repo):
319 def reposetup(ui, repo):
317 uisetup(repo.ui)
320 uisetup(repo.ui)
General Comments 0
You need to be logged in to leave comments. Login now