Please note that the CVS and issue trackers have moved to GitHub. These Trac pages are no longer kept up-to-date.

root/seattle/trunk/resource/benchmark_resources.py@5621

Revision 5621, 23.0 KB (checked in by justinc, 7 years ago)

print statement fix. If they donate != 10% it was wrong.

Line 
1"""
2<Program Name>
3  benchmark_resources.repy
4
5<Started>
6  2009-08-1
7
8<Author>
9  Anthony Honstain
10  Modified by Steven Portzer
11 
12<Purpose>
13  Will use the file 'vesselinfo' which should already be in the directory
14  (which is constructed by the website) and runs a OS specific benchmark
15  to construct the vessels defined in vesselinfo with the correct resource
16  files so that seattle only the donated resources.
17 
18  A wiki page concerning this module can be found at:
19  https://seattle.cs.washington.edu/wiki/BenchmarkCustomInstallerInfo
20  Which contains a more high level overview of the operation.
21 
22  This should be run internally by seattleinstaller.py
23
24<Resource events>
25  WARNING the 'events' resource is hardcoded to a value of 500
26 
27  The 'events' resource has proven very difficult to measure across the
28  different operating systems, and on some it is infeasible to measure.
29  The decision has been made to take 500 events for the node, regardless
30  of the percentage the user will donate. The value of 500 is inserted
31  in the benchmark_resources.get_donated_from_maxresources.
32 
33  Warning: there is still a default value in the DEFAULT_MAX_RESOURCE_DICT
34  and run_benchmark will return a dict with the default or what ever the
35  OS specific script returned (they should be set to return None for events).
36  This is confusing, but I chosen to do this for the sake of future
37  modifications, if we found a way to measure the number of events or the
38  meaning of how we use them changed completely, it would be easy to
39  transition back to getting a benchmark for the resource.
40
41"""
42
43
44import nonportable
45import create_installer_state
46import nmresourcemath
47import sys
48import traceback
49import platform # for detecting Nokia tablets
50
51
52# These are the default maximum values that we assume a computer to have
53# These values will be used in the event the OS specific script is unable
54# to determine a maximum for the value.
55# These were the lower numbers returned from developer testing.
56DEFAULT_MAX_RESOURCE_DICT = {"cpu":1,
57                             "memory":510000000, # allmost 512MB
58                             "diskused":3700000000, # 3.44589 GB
59                             "events":500, # see module's doc string.
60                             "filesopened":250,
61                             "insockets":250,
62                             "outsockets":250,
63                             "random":200000,
64                             "filewrite":1200000,
65                             "fileread":1200000,
66                             "netrecv":500000,
67                             "netsend":500000,
68                             "lograte":1500000,
69                             "loopsend":50000000,
70                             "looprecv":50000000}
71
72# Default resources that define the cost of splitting a vessel
73DEFAULT_OFFCUT_DICT =  {'cpu':.002,
74                        'memory': 1000000,   # 1 MiB
75                        'diskused': 100000, # .1 MiB
76                        'events':2,
77                        'filewrite':1000,
78                        'fileread':1000,
79                        'filesopened':1, 
80                        'insockets':0,
81                        'outsockets':0,
82                        'netsend':0,
83                        'netrecv':0,
84                        'loopsend':0,  # would change with prompt functionality (?)
85                        'looprecv':0,
86                        'lograte':100, # the monitor might log something
87                        'random':0 }
88
89if platform.machine().startswith('armv'):
90  # there is not enough filewrite/fileread resources on the Nokia so their
91  # requirements should be decreased.
92  # AR: This also catches Android and iDevices.
93  DEFAULT_OFFCUT_DICT['filewrite'] = 800
94  DEFAULT_OFFCUT_DICT['fileread'] = 800
95
96# Current this is only raised when nmresourcemath raises a ResourceParseError.
97# I suppose we could let the nmresourcemath exception propagate instead
98# of catching it and raising out own, but I was hoping this would provide
99# more useful information and possibly be more useful if at some point
100# this module takes over some of the functionality of nmresourcemath, or
101# performs more checking on initial resources.
102class InsufficientResourceError(Exception):
103  """Error to indicate that vessels cannot be created with given resources."""
104  pass
105
106class BenchmarkingFailureError(Exception):
107  """
108  Error to indicate that benchmarking failures occurred and installation should
109  not continue.
110  """
111  pass
112
113
114
115def log_failure(errorstring, logfileobj):
116  """
117  <Purpose>
118    To log benchmarking failures to the log file and print it so the user can
119    tell what errors occurred.
120
121    This function exists to keep run_benchmark a bit cleaner.
122       
123  <Arguments>
124    errorstring: The string describing the benchmarking failure being logged.
125
126    logfileobj: The open file object that will be used for logging
127        the benchmarking failure.
128   
129  <Exceptions>
130    None
131   
132  <Side Effects>
133    Will log failure to logfileob and print it to system output.
134 
135  <Return>
136    None
137     
138  """
139
140  logfileobj.write(errorstring + "\n")
141  print errorstring
142
143
144def prompt_user(promptstring):
145  """
146  <Purpose>
147    To receive yes or no input from the user. Capitalization is ignored and
148    y or n will also be accepted.
149       
150  <Arguments>
151    promptstring: The string to display when prompting the user for a yes or no
152      input.
153   
154  <Exceptions>
155    None
156   
157  <Side Effects>
158    Prints promptstring and waits for a yes or no input from the user.
159 
160  <Return>
161    True if the user responds with yes and false if no.
162     
163  """
164
165  userinput = raw_input(promptstring)
166
167  while True:
168    userinput = userinput.lower()
169
170    if userinput == "yes" or userinput == "y":
171      return True
172    if userinput == "no" or userinput == "n":
173      return False
174
175    userinput = raw_input("Please enter either yes or no: ")
176
177
178def run_benchmark(logfileobj):
179  """
180  <Purpose>
181    To run the individual OS specific scripts and supplement their results
182    (if they do not returned a benchmark for every resource) with default
183    values if needed.
184   
185    This method has some cut and paste, but it seemed to be the simplest
186    way to handle the OS specific imports and logging.
187   
188    WARNING the dictionary returned still treats cpu as an integer representing
189    the number of processors (not a float like it will be later in the process).
190       
191  <Arguments>
192    logfileobj: The open file object that will be used for logging
193        the benchmark process and the creation of the installer state.
194   
195  <Exceptions>
196    BenchmarkingFailureError: Indicates that one or more benchmark failed and
197      the user opted to terminate installation.
198   
199  <Side Effects>
200    May use the drive to measure read/write, network resources to measure
201    bandwidth, and request OS sources of crypto-graphically strong
202    pseudo-random numbers.
203    Will use servicelogger to log benchmark failures to 'installInfo'.
204 
205  <Return>
206    A dictionary with measurement value for all the resources in the dict.
207     
208  """
209  OS = nonportable.ostype
210 
211  max_resource_dict = None
212  benchmarking_failed = False
213
214  # The imports are all done based on OS because the scripts cannot be
215  # imported into a different OS. I can not import the Windows script on
216  # Linux because I lack required libraries etc..
217  # If any of them crash we want to log it and continue with default values.
218  if OS == "Windows":
219    try:
220      import Win_WinCE_resources
221      max_resource_dict = Win_WinCE_resources.measure_resources() 
222    except Exception:
223      log_failure("Failed to benchmark Windows OS.", logfileobj)
224      exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
225      traceback.print_exception(exceptionType, exceptionValue, \
226                                exceptionTraceback, file=logfileobj)
227      max_resource_dict = DEFAULT_MAX_RESOURCE_DICT.copy()
228      benchmarking_failed = True
229     
230  elif OS == "WindowsCE":
231    try:
232      import Win_WinCE_resources
233      max_resource_dict = Win_WinCE_resources.measure_resources()
234    except Exception:
235      log_failure("Failed to benchmark WindowsCE OS.", logfileobj)
236      exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
237      traceback.print_exception(exceptionType, exceptionValue, \
238                                exceptionTraceback, file=logfileobj)
239      max_resource_dict = DEFAULT_MAX_RESOURCE_DICT.copy()
240      benchmarking_failed = True
241   
242  elif OS == "Linux":
243    try:
244      import Linux_resources
245      max_resource_dict = Linux_resources.measure_resources()
246      try:
247        import android
248        is_android = True
249        import os
250        # Use environmental variables to pass data from Java->Python on Android
251        cores = long(os.environ.get('SEATTLE_AVAILABLE_CORES', 0))
252        if cores > 0:
253          max_resource_dict["cpu"] = cores
254        diskused = long(os.environ.get('SEATTLE_AVAILABLE_SPACE', 0))
255        if diskused > 0:
256          max_resource_dict["diskused"] = diskused
257      except ImportError:
258        is_android = False
259    except Exception:
260      log_failure("Failed to benchmark Linux OS.", logfileobj)
261      exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
262      traceback.print_exception(exceptionType, exceptionValue, \
263                                exceptionTraceback, file=logfileobj)
264      max_resource_dict = DEFAULT_MAX_RESOURCE_DICT.copy()
265      benchmarking_failed = True
266     
267  elif OS == "Darwin":
268    try:
269      import Mac_BSD_resources
270      max_resource_dict = Mac_BSD_resources.measure_resources()
271    except Exception:
272      log_failure("Failed to benchmark Darwin OS.", logfileobj)
273      exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
274      traceback.print_exception(exceptionType, exceptionValue, \
275                                exceptionTraceback, file=logfileobj)
276      max_resource_dict = DEFAULT_MAX_RESOURCE_DICT.copy()
277      benchmarking_failed = True
278  else:
279    raise nonportable.UnsupportedSystemException("The operating system '" \
280              + OS + "' is not supported.")
281   
282  # We are going to log the benchmarked system resources, this will be
283  # very useful in the event a user chooses to share the data with us.
284  if not benchmarking_failed:
285    logfileobj.write("Total resources measured by the script for " + OS + \
286                   " OS: " + str(max_resource_dict) + "\n")   
287   
288  # The dictionary returned by the scripts will contain null values for
289  # resources that they were not benchmarked. If a benchmark failed, the
290  # dictionary will contain a string describing the failure that occurred.
291  for resource in DEFAULT_MAX_RESOURCE_DICT:
292   
293    # Make sure the benchmarking script actually returned something for the
294    # given resource. This should be the case, but if the scripts are
295    # changed at some point, then the problem will be caught here.
296    if resource not in max_resource_dict:
297      log_failure("Benchmark script did not return value for " + resource, \
298                         logfileobj)
299      max_resource_dict[resource] = DEFAULT_MAX_RESOURCE_DICT[resource]
300      benchmarking_failed = True
301
302    # For all the null values, we want to set a default.
303    elif max_resource_dict[resource] is None:
304      max_resource_dict[resource] = DEFAULT_MAX_RESOURCE_DICT[resource]
305
306    # If the value is a string, then the benchmark failed, so we want to
307    # log the failure and use the default value.
308    elif isinstance(max_resource_dict[resource], basestring):
309      log_failure("Benchmark failed for " + resource + " resource: " +
310                    max_resource_dict[resource], logfileobj)
311      max_resource_dict[resource] = DEFAULT_MAX_RESOURCE_DICT[resource]
312      benchmarking_failed = True
313
314    else: 
315      # This is done for added security in case scripts gets changed or
316      # modified in the future, it will get caught here if the scripts
317      # are not catching bad values (since they all reach out to other
318      # OS specific files and programs).
319      try:
320        max_resource_dict[resource] = int(max_resource_dict[resource])
321      except ValueError, e:
322        log_failure("Benchmark script had bad value for " + resource \
323                           + ": " + str(max_resource_dict[resource]), logfileobj)
324        max_resource_dict[resource] = DEFAULT_MAX_RESOURCE_DICT[resource]
325        benchmarking_failed = True
326     
327      if max_resource_dict[resource] <= 0:
328        log_failure("Benchmark script had non-positive value for " + resource \
329                           + ": " + str(max_resource_dict[resource]), logfileobj)
330        max_resource_dict[resource] = DEFAULT_MAX_RESOURCE_DICT[resource]
331        benchmarking_failed = True
332       
333  # If one or more benchmark failed, then we want to give the user the option
334  # of aborting the installation
335  if benchmarking_failed:
336    print "The above benchmarking error(s) occurred."
337    print "If you choose to continue anyways then default values will be " + \
338                    "used for failed benchmarks."
339
340    if not is_android:
341      continue_install = prompt_user("Continue with installation? (yes/no) ")
342
343    # AR: This pops up the actual dialog on the screen on Android
344    else:
345      droid = android.Android()
346      droid.dialogCreateAlert("Benchmarking error(s) occurred",
347        "If you choose to continue, then default values will be used for failed benchmarks.")
348      droid.dialogSetPositiveButtonText("Continue")
349      droid.dialogSetNegativeButtonText("Cancel")
350      droid.dialogShow()
351      response = droid.dialogGetResponse().result
352      droid.dialogDismiss()
353      if response.has_key("which"):
354        result = response["which"]
355        if result == "positive":
356          continue_install = True
357        else:
358          continue_install = False
359
360    if not continue_install:
361      logfileobj.write("Installation terminated by user after one or " + \
362                      "more failed benchmarks.\n")
363      raise BenchmarkingFailureError()
364
365    logfileobj.write("Installation continued by user. Default values are " + \
366                    "being used for failed benchmarks.\n")
367
368  # These are the resources the script will use to calculate the donated
369  # resources, I am going to log this just to be safe.     
370  logfileobj.write("Final checked resources that we will use: " + \
371                   str(max_resource_dict) + "\n")
372 
373  # There should be no missing keys.
374  # An example of what could now be contained in max_resource_dict
375  # {'diskused': 63202701312L, 'fileread': 35140832, 'filewrite': 35140832,
376  #  'loopsend': 1000000, 'lograte': 30000, 'netrecv': 100000,
377  #  'random': 27776, 'insockets': 1024, 'filesopened': 1024,
378  #  'looprecv': 1000000, 'cpu': 2, 'memory': 2047868000L,
379  #  'netsend': 100000, 'outsockets': 1024, 'events': 32620}
380  return max_resource_dict
381
382
383
384def get_donated_from_maxresources(max_resources_dict, resource_percent):
385  """
386  <Purpose>
387    To get the portion of system resources requested by the user, this
388    will be the user determined percentage of each resource in
389    max_resources_dict.
390   
391  <Arguments>
392    max_resource_dict: dictionary containing the total amount of each
393      resource that is available on the machine. 
394 
395    resource_percent: The number representing the percent of system resources
396        that will be donated to seattle (Normally 10 is requested).
397       
398  <Exceptions>
399    None
400   
401  <Side Effects>
402    None
403 
404  <Return>
405    Dictionary with the same keys as the DEFAULT_MAX_RESOURCE_DICT that
406    contains the donated system resources.
407 
408  """   
409 
410  donatedresources = {} 
411  resource_percent /= 100.0
412 
413  # So far only cpu and events are handled specially. Computed result should
414  # be an integer
415  for resource in max_resources_dict:
416    if resource == 'cpu':
417      # Result should be a float in [0,1] (while most other
418      # values in the resource file are integers).
419      tmp_resource = (max_resources_dict['cpu'] * resource_percent)
420      donatedresources[resource] = tmp_resource
421    elif resource == 'events':
422      # Given the difficulty of accurately measuring this on
423      # most systems, 500 was chosen as a reasonable value, if
424      # for some reason a system has less, the node will deal
425      # with that later.
426      donatedresources[resource] = 500     
427    else:
428      tmp_resource = int(max_resources_dict[resource] * resource_percent)
429      donatedresources[resource] = tmp_resource     
430     
431  return donatedresources
432
433
434   
435def get_tenpercent_of_donated(donatedresources):
436  """
437  <Purpose>
438    To get ten percent of the resources that have been donated (offcut
439    resources for the multiple vessels has already been removed).
440   
441  <Arguments>
442    max_resource_dict: dictionary containing the total amount of each
443      donated resource on the machine. 
444       
445  <Exceptions>
446    None
447   
448  <Side Effects>
449    None
450 
451  <Return>
452    Dictionary with the same keys as the DEFAULT_MAX_RESOURCE_DICT that
453    contains ten percent of the donated system resources.
454 
455  """   
456 
457  tenpercent = {} 
458 
459  # So far only cpu is handled specially, if others needed special
460  # consideration it could be added here. Computed result should
461  # be an integer
462  for resource in donatedresources:
463    if resource == 'cpu':
464      # Result should be a float in [0,1] (while most other
465      # values in the resource file are integers).
466      tenpercent[resource] = donatedresources['cpu'] * .1   
467    else:
468      tenpercent[resource] = int(donatedresources[resource] * .1)   
469     
470  return tenpercent
471
472
473
474def main(prog_path, resource_percent, logfileobj):
475  """
476  <Purpose>
477    To run the benchmarks and use the writecustominstaller to create
478    the state for the vessels (resource files and directories).
479   
480  <Arguments>
481    prog_path:
482      Path to the directory in which seattle is being installed.
483   
484    resource_percent: The number representing the percent of system resources
485        that will be donated to seattle (Normally 10 is requested).
486       
487    logfileobj: The open file object that will be used for logging
488        the benchmark process and the creation of the installer state.
489   
490  <Exceptions>
491    InsufficientResourceError: Exception to indicate that there was
492      and insufficient amount of resources. This could be because
493      the benchmarks were very low or because there are to many
494      vessels for a specific resource quantity.
495 
496    We let the exceptions raised by create_installer_state propogate up
497    since they indicate this installation will completely fail and must
498    be terminated.
499   
500    The following are exceptions that are raised by create_installer_state:
501     
502      InvalidVesselInfoError: Exception to indicate an improper vesselinfo
503        file, it means a vessel did not have an owner or the file was corrupted
504        or improperly formated.
505     
506      ValueError: if the vessel percent (from element 0 of a tuple in the
507        vesselcreationlist list) is not 10,20,30, ... , 80, 90, 100
508        A ValueError will also be raised if the total percent (of all
509        the vessel percents in the vesselcreationlist list) does not add
510        up to the integer 100
511 
512      IOError: if vessel.restrictions is not found, this must be included
513        ahead of time in the target directory.
514   
515      OSError, WindowsError: if os.mkdir is unable to create the vessel
516        directories.
517
518    Installation will also fail if run_benchmark raises the following exception:
519
520      BenchmarkingFailureError: Indicates that one or more benchmark failed and
521        the user opted to terminate installation.
522 
523 
524  <Side Effects>
525    Will log to the logfileob that was passed as an argument if there is
526    a failure.
527    Writes out resource.v1, resource.v2, ... vessel resources files and
528    creates directories v1,v2,...
529   
530  <Return>
531    None
532 
533  """
534
535  logfileobj.write("New installation, beginning benchmark.\n")
536   
537  # Read the vesselinfo file, this may be corrupted or if it is put together
538  # wrong by the server we let the exception propagate in order to stop the
539  # install because it would result in a bad node. 
540  try:
541    vesselcreationlist = create_installer_state.read_vesselinfo_from_file("vesselinfo")
542  except Exception:
543    vesselinfodata = open("vesselinfo",'r')
544    logfileobj.write(vesselinfodata.read() + "\n")
545    vesselinfodata.close()
546    raise
547 
548  max_resources_dict = run_benchmark(logfileobj) 
549 
550  # I am logging the percentage that should donated to make it easier
551  # to track down the cause of exceptions related to resource splitting.
552  logfileobj.write("User intended to donate :" + str(resource_percent) + \
553                   " percent.\n")
554 
555  # Take the max resources and get the donated resources.
556  donatedresources = get_donated_from_maxresources(max_resources_dict, 
557                                                   resource_percent)
558  logfileobj.write("Donated resources:" + str(donatedresources) + "\n")
559 
560 
561  # Find the number of vessels and that the initial node should contain
562  # and deduct the appropriate offcut resources to account for all the vessels.
563  vesselcount = 0
564  for item in vesselcreationlist:
565    vesselcount += 1
566 
567  for i in range(vesselcount):
568    donatedresources = nmresourcemath.subtract(donatedresources, 
569                                               DEFAULT_OFFCUT_DICT)
570 
571  # ensure there aren't negative resources, we will log this and raise
572  # an exception for seattleinstaller to catch.
573  try:
574    nmresourcemath.check_for_negative_resources(donatedresources)
575  except nmresourcemath.ResourceParseError, e:
576    logfileobj.write("donatedresources that contain a negative resource" +  \
577                      str(donatedresources) + "\n")
578    logfileobj.write("Insufficient resources for desired number of " + \
579                      "vessels :" + str(e) + "\nThis means that after " + \
580                      "accounting for the resources.offcut (the cost of " + \
581                      "splitting a vessel) there were negative " + \
582                      "resources.\n")
583   
584   
585    raise InsufficientResourceError("Cost of splitting vessels resulted in " + \
586                                   "negative resource values.")
587   
588  # ten percent is selected to make the job of create_installer_state as
589  # simple as possible, it requires vessels be alloted from the donated
590  # resources in increments of 10 percent. If we allowed vessels to be
591  # alloted portions like 22% of the donated resources we would need to
592  # split the donated resources down farther and the likely hood of vessels
593  # getting little or none of a resource increases. It could be possible
594  # to improve this in the future but for now this seems safest, especially
595  # since some of the benchmark scripts pick very safe/low values.
596  tenpercentdict = get_tenpercent_of_donated(donatedresources)
597  # Going to go ahead and log it just to be safe.
598  logfileobj.write("Useful amount of the donatedresources (offcut costs " + \
599                    "removed already): " + str(tenpercentdict) + "\n")
600 
601  # Create the installer installer initial vessel state, this will create
602  # the vesseldict, vessel directories, and vessel resource files.
603  # Note: possible exceptions for this module are: ValueError, IOError,
604  # OSError, WindowsError. But they will be allowed to propagate up.
605  create_installer_state.main(vesselcreationlist, tenpercentdict, prog_path)
Note: See TracBrowser for help on using the browser.