Commit aeeb394b00d0b457b8797ec283b1776f5a5ac400

Authored by Jürgen Knödlseder
1 parent b15660ff

Refactor GDaemon class and add more statistics information in XML file (#3592)

include/GDaemon.hpp
... ... @@ -36,6 +36,7 @@
36 36  
37 37 /* __ Forward declarations _______________________________________________ */
38 38 class GCsv;
  39 +class GXml;
39 40  
40 41 /* __ Constants __________________________________________________________ */
41 42  
... ... @@ -79,7 +80,13 @@ protected:
79 80 GFilename statistics_filename(void) const;
80 81 pid_t lock_pid(void) const;
81 82 void update_statistics(void);
82   - void update_high_level_statistics(const GCsv& statistics);
  83 + void update_xml(const GCsv& statistics);
  84 + void create_xml(const GFilename& filename);
  85 + void update_dates(GXml& xml, const GCsv& statistics);
  86 + void update_countries_header(GXml& xml, const GCsv& statistics);
  87 + void update_countries_data(GXml& xml, const GCsv& statistics);
  88 + void update_versions_data(GXml& xml, const GCsv& statistics);
  89 + void update_daily(GXml& xml, const GCsv& statistics);
83 90  
84 91 // Protected members
85 92 pid_t m_pid; //!< Process ID
... ...
src/support/GDaemon.cpp
... ... @@ -439,8 +439,6 @@ pid_t GDaemon::lock_pid(void) const
439 439 *
440 440 * Update application statistics in the @p statistics.xml file by scanning
441 441 * the low-level file @p statistics.csv.
442   - *
443   - * @todo Delete low-level file if scan was successful.
444 442 ***************************************************************************/
445 443 void GDaemon::update_statistics(void)
446 444 {
... ... @@ -471,7 +469,7 @@ void GDaemon::update_statistics(void)
471 469 GCsv statistics(filename, ",");
472 470  
473 471 // Update high-level statistics
474   - update_high_level_statistics(statistics);
  472 + update_xml(statistics);
475 473  
476 474 // Remove low-level statistics file
477 475 std::remove(filename.url().c_str());
... ... @@ -501,93 +499,177 @@ void GDaemon::update_statistics(void)
501 499 * The high-level statistics is written out as XML file. If no XML file
502 500 * exists a new one is created, otherwise an existing file is updated.
503 501 ***************************************************************************/
504   -void GDaemon::update_high_level_statistics(const GCsv& statistics)
  502 +void GDaemon::update_xml(const GCsv& statistics)
505 503 {
506 504 // Log updating of high-level statistics
507 505 m_log << "Update high-level statistics" << std::endl;
508 506  
509   - // Set current time
510   - GTime now;
511   - now.now();
512   -
513 507 // Set high-level statistics filename
514 508 GFilename filename = gammalib::gamma_filename("statistics.xml");
515 509  
516   - // Initialise high-level statistics
517   - GXml xml;
  510 + // If no high-level statistics file exists then create one
  511 + if (!filename.exists()) {
  512 + create_xml(filename);
  513 + }
518 514  
519   - // If a high-level statistics file exists already then load information
520   - // from the file
521   - if (filename.exists()) {
  515 + // Load high-level statistics file. If an exception occurs during this
  516 + // process the input file is probably corrupt, and a failure message
  517 + // is written into the log file. Processing only continues if no
  518 + // exception occured.
  519 + GXml xml;
  520 + try {
522 521 xml.load(filename);
523 522 }
  523 + catch (...) {
  524 + xml.clear();
  525 + m_log << "*** Failure occured in loading high-level statistics ";
  526 + m_log << "XML file \"" << filename.url() << "\"" << std::endl;
  527 + }
524 528  
525   - // ... otherwise create empty high-level statistics file
526   - else {
  529 + // Update file only if XML document is not empty
  530 + if (!xml.is_empty()) {
527 531  
528   - // Append base
529   - GXmlNode* base = xml.append(GXmlElement("statistics title=\"High-level statistics\""));
  532 + // Update dates in header
  533 + update_dates(xml, statistics);
  534 +
  535 + // Update countries in header and data
  536 + update_countries_header(xml, statistics);
  537 + update_countries_data(xml, statistics);
530 538  
531   - // Append header
532   - GXmlNode* header = base->append("header");
533   - GXmlNode* dates = header->append("dates");
534   - GXmlNode* countries = header->append("countries");
535   - GXmlNode* d1 = dates->append("creation");
536   - d1->append(GXmlText(now.utc()));
537   - GXmlNode* d2 = dates->append("modified");
538   - d2->append(GXmlText(now.utc()));
539   - GXmlNode* d3 = dates->append("start");
540   - d3->append(GXmlText(""));
541   - GXmlNode* d4 = dates->append("stop");
542   - d4->append(GXmlText(""));
543   -
544   - // Append data
545   - GXmlNode* data = base->append("data");
546   - GXmlNode* daily = data->append("daily");
547   -
548   - // Log creation of high-level statistics file
549   - m_log << "Created high-level statistics XML file \"";
550   - m_log << filename.url() << "\"" << std::endl;
551   -
552   - } // endelse: created empty high-level statistics file
553   -
554   - // Get some useful pointers
  539 + // Update versions in data
  540 + update_versions_data(xml, statistics);
  541 +
  542 + // Update daily information
  543 + update_daily(xml, statistics);
  544 +
  545 + // Save updated high-level statistics XML file
  546 + xml.save(filename);
  547 +
  548 + } // endif: XML document was not empty
  549 +
  550 + // Return
  551 + return;
  552 +}
  553 +
  554 +
  555 +/***********************************************************************//**
  556 + * @brief Create high-level statistics XML file
  557 + *
  558 + * @param[in] filename File name.
  559 + *
  560 + * Creates an empty high-level statistics XML file with the specified
  561 + * @p filename. This method overwrites any existing XML file.
  562 + ***************************************************************************/
  563 +void GDaemon::create_xml(const GFilename& filename)
  564 +{
  565 + // Initialise high-level statistics XML object
  566 + GXml xml;
  567 +
  568 + // Append base node
  569 + GXmlNode* base = xml.append(GXmlElement("statistics title=\"High-level statistics\""));
  570 +
  571 + // Append header
  572 + GXmlNode* header = base->append("header");
  573 + GXmlNode* dates = header->append("dates");
  574 + GXmlNode* countries = header->append("countries");
  575 + GXmlNode* d1 = dates->append("creation");
  576 + d1->append(GXmlText(""));
  577 + GXmlNode* d2 = dates->append("modified");
  578 + d2->append(GXmlText(""));
  579 + GXmlNode* d3 = dates->append("start");
  580 + d3->append(GXmlText(""));
  581 + GXmlNode* d4 = dates->append("stop");
  582 + d4->append(GXmlText(""));
  583 +
  584 + // Append data
  585 + GXmlNode* data = base->append("data");
  586 + data->append("countries");
  587 + data->append("versions");
  588 + data->append("daily");
  589 +
  590 + // Log creation of high-level statistics file
  591 + m_log << "Created high-level statistics XML file \"";
  592 + m_log << filename.url() << "\"" << std::endl;
  593 +
  594 + // Save high-level statistics XML file
  595 + xml.save(filename);
  596 +
  597 + // Return
  598 + return;
  599 +}
  600 +
  601 +
  602 +/***********************************************************************//**
  603 + * @brief Update dates in high-level statistics header.
  604 + *
  605 + * @param[in,out] xml High-level statistics XML object.
  606 + * @param[in] statistics Low-level statistics CSV object.
  607 + *
  608 + * Updates dates in the high-level statistics header.
  609 + ***************************************************************************/
  610 +void GDaemon::update_dates(GXml& xml, const GCsv& statistics)
  611 +{
  612 + // Set current time
  613 + GTime now;
  614 + now.now();
  615 +
  616 + // Get pointer to "header" node
555 617 GXmlNode* base = xml.element("statistics",0);
556 618 GXmlNode* header = base->element("header",0);
557   - GXmlNode* data = base->element("data",0);
558   - GXmlNode* daily = data->element("daily",0);
559 619  
560   - // Compatibility code: Make sure that a "countries" element exist
  620 + // Update modified time in header
  621 + GXmlNode* modified = header->element("dates > modified");
  622 + static_cast<GXmlText*>((*modified)[0])->text(now.utc());
  623 +
  624 + // Update first and last time in header
  625 + std::string start;
  626 + std::string stop;
  627 + if (statistics.nrows() > 1) {
  628 + start = statistics.string(1,0);
  629 + stop = statistics.string(statistics.nrows()-1,0);
  630 + GXmlNode* n_start = header->element("dates > start");
  631 + GXmlNode* n_stop = header->element("dates > stop");
  632 + std::string current_start = static_cast<GXmlText*>((*n_start)[0])->text();
  633 + std::string current_stop = static_cast<GXmlText*>((*n_stop)[0])->text();
  634 + if (current_start.empty() || (start < current_start)) {
  635 + static_cast<GXmlText*>((*n_start)[0])->text(start);
  636 + }
  637 + if (current_stop.empty() || (stop > current_stop)) {
  638 + static_cast<GXmlText*>((*n_stop)[0])->text(stop);
  639 + }
  640 + }
  641 +
  642 + // Return
  643 + return;
  644 +}
  645 +
  646 +
  647 +/***********************************************************************//**
  648 + * @brief Update countries in high-level statistics header.
  649 + *
  650 + * @param[in,out] xml High-level statistics XML object.
  651 + * @param[in] statistics Low-level statistics CSV object.
  652 + *
  653 + * Update country statistics in header block.
  654 + ***************************************************************************/
  655 +void GDaemon::update_countries_header(GXml& xml, const GCsv& statistics)
  656 +{
  657 + // Get pointer to "countries" node in "header" node. In case that no
  658 + // "countries" node exists in "header" node then append one.
  659 + GXmlNode* base = xml.element("statistics",0);
  660 + GXmlNode* header = base->element("header",0);
561 661 if (header->elements("countries") == 0) {
562 662 header->append("countries");
563 663 }
564   -
565   - // Get more useful pointers
566 664 GXmlNode* countries = header->element("countries",0);
567 665  
568 666 // Loop over statistics
569   - for (int i = 0; i < statistics.nrows(); ++i) {
570   -
571   - // Skip header row
572   - if (i == 0) {
573   - continue;
574   - }
  667 + for (int i = 1; i < statistics.nrows(); ++i) {
575 668  
576 669 // Extract relevant attributes
577   - std::string date = statistics.string(i,0).substr(0,10);
578 670 std::string country = statistics.string(i,2);
579   - std::string tool = statistics.string(i,3);
580   - std::string version = statistics.string(i,4);
581   - double wall = statistics.real(i,5);
582   - double cpu = statistics.real(i,6);
583   - double gCO2e = statistics.real(i,7);
584   -
585   - // Skip tools without name
586   - if (tool.empty()) {
587   - continue;
588   - }
589 671  
590   - // Update list of countries
  672 + // Update list of countries in header
591 673 int ncountries = countries->elements("country");
592 674 if (ncountries == 0) {
593 675 GXmlNode* n = countries->append("country");
... ... @@ -607,111 +689,283 @@ void GDaemon::update_high_level_statistics(const GCsv&amp; statistics)
607 689 }
608 690 }
609 691  
610   - // Get pointer to relevant date element. If no date element exists
  692 + } // endfor: looped over statistics
  693 +
  694 + // Return
  695 + return;
  696 +}
  697 +
  698 +
  699 +/***********************************************************************//**
  700 + * @brief Update countries in high-level statistics data.
  701 + *
  702 + * @param[in,out] xml High-level statistics XML object.
  703 + * @param[in] statistics Low-level statistics CSV object.
  704 + *
  705 + * Update country statistics in data block.
  706 + ***************************************************************************/
  707 +void GDaemon::update_countries_data(GXml& xml, const GCsv& statistics)
  708 +{
  709 + // Get pointer to "countries" node. In case that no "countries" node
  710 + // exists in "data" node then append one.
  711 + GXmlNode* base = xml.element("statistics",0);
  712 + GXmlNode* data = base->element("data",0);
  713 + if (data->elements("countries") == 0) {
  714 + data->append("countries");
  715 + }
  716 + GXmlNode* countries = data->element("countries",0);
  717 +
  718 + // Loop over statistics
  719 + for (int i = 1; i < statistics.nrows(); ++i) {
  720 +
  721 + // Extract relevant attributes
  722 + std::string country = statistics.string(i,2);
  723 + double wall = statistics.real(i,5);
  724 + double cpu = statistics.real(i,6);
  725 + double gCO2e = statistics.real(i,7);
  726 +
  727 + // Get pointer to relevant "country" element. If no "country"
  728 + // element exists that corresponds to the country of the tool then
  729 + // append a new element
  730 + GXmlElement* country_element = NULL;
  731 + int ncountries = countries->elements();
  732 + if (ncountries == 0) {
  733 + country_element = countries->append(country);
  734 + }
  735 + else {
  736 + for (int k = 0; k < ncountries; ++k) {
  737 + GXmlElement* element = countries->element(k);
  738 + if (element->name() == country) {
  739 + country_element = element;
  740 + break;
  741 + }
  742 + }
  743 + if (country_element == NULL) {
  744 + country_element = countries->append(country);
  745 + }
  746 + }
  747 +
  748 + // Now we have the relevant "country" element and we can update the
  749 + // statistics
  750 + int calls = 1;
  751 + if (country_element->has_attribute("calls")) {
  752 + calls += gammalib::toint(country_element->attribute("calls"));
  753 + }
  754 + if (country_element->has_attribute("wall")) {
  755 + wall += gammalib::todouble(country_element->attribute("wall"));
  756 + }
  757 + if (country_element->has_attribute("cpu")) {
  758 + cpu += gammalib::todouble(country_element->attribute("cpu"));
  759 + }
  760 + if (country_element->has_attribute("gCO2e")) {
  761 + gCO2e += gammalib::todouble(country_element->attribute("gCO2e"));
  762 + }
  763 + country_element->attribute("calls", gammalib::str(calls));
  764 + country_element->attribute("wall", gammalib::str(wall));
  765 + country_element->attribute("cpu", gammalib::str(cpu));
  766 + country_element->attribute("gCO2e", gammalib::str(gCO2e));
  767 +
  768 + } // endfor: looped over statistics
  769 +
  770 + // Return
  771 + return;
  772 +}
  773 +
  774 +
  775 +/***********************************************************************//**
  776 + * @brief Update versions in high-level statistics data.
  777 + *
  778 + * @param[in,out] xml High-level statistics XML object.
  779 + * @param[in] statistics Low-level statistics CSV object.
  780 + *
  781 + * Update version statistics in data block.
  782 + ***************************************************************************/
  783 +void GDaemon::update_versions_data(GXml& xml, const GCsv& statistics)
  784 +{
  785 + // Get pointer to "versions" node. In case that no "versions" node exists
  786 + // in "data" node then append one.
  787 + GXmlNode* base = xml.element("statistics",0);
  788 + GXmlNode* data = base->element("data",0);
  789 + if (data->elements("versions") == 0) {
  790 + data->append("versions");
  791 + }
  792 + GXmlNode* versions = data->element("versions",0);
  793 +
  794 + // Loop over statistics
  795 + for (int i = 1; i < statistics.nrows(); ++i) {
  796 +
  797 + // Extract relevant attributes
  798 + std::string version = statistics.string(i,4);
  799 + double wall = statistics.real(i,5);
  800 + double cpu = statistics.real(i,6);
  801 + double gCO2e = statistics.real(i,7);
  802 +
  803 + // Get pointer to relevant "version" element. If no "version"
  804 + // element exists that corresponds to the version of the tool then
  805 + // append a new element
  806 + GXmlElement* version_element = NULL;
  807 + int nversion = versions->elements();
  808 + if (nversion == 0) {
  809 + version_element = versions->append(version);
  810 + }
  811 + else {
  812 + for (int k = 0; k < nversion; ++k) {
  813 + GXmlElement* element = versions->element(k);
  814 + if (element->name() == version) {
  815 + version_element = element;
  816 + break;
  817 + }
  818 + }
  819 + if (version_element == NULL) {
  820 + version_element = versions->append(version);
  821 + }
  822 + }
  823 +
  824 + // Now we have the relevant "version" element and we can update the
  825 + // statistics
  826 + int calls = 1;
  827 + if (version_element->has_attribute("calls")) {
  828 + calls += gammalib::toint(version_element->attribute("calls"));
  829 + }
  830 + if (version_element->has_attribute("wall")) {
  831 + wall += gammalib::todouble(version_element->attribute("wall"));
  832 + }
  833 + if (version_element->has_attribute("cpu")) {
  834 + cpu += gammalib::todouble(version_element->attribute("cpu"));
  835 + }
  836 + if (version_element->has_attribute("gCO2e")) {
  837 + gCO2e += gammalib::todouble(version_element->attribute("gCO2e"));
  838 + }
  839 + version_element->attribute("calls", gammalib::str(calls));
  840 + version_element->attribute("wall", gammalib::str(wall));
  841 + version_element->attribute("cpu", gammalib::str(cpu));
  842 + version_element->attribute("gCO2e", gammalib::str(gCO2e));
  843 +
  844 + } // endfor: looped over statistics
  845 +
  846 + // Return
  847 + return;
  848 +}
  849 +
  850 +
  851 +/***********************************************************************//**
  852 + * @brief Update daily statistics
  853 + *
  854 + * @param[in,out] xml High-level statistics XML object.
  855 + * @param[in] statistics Low-level statistics CSV object.
  856 + *
  857 + * Update daily statistics in data block.
  858 + ***************************************************************************/
  859 +void GDaemon::update_daily(GXml& xml, const GCsv& statistics)
  860 +{
  861 + // Get pointer to "daily" node. In case that no "daily" node exists
  862 + // in "data" node then append one.
  863 + GXmlNode* base = xml.element("statistics",0);
  864 + GXmlNode* data = base->element("data",0);
  865 + if (data->elements("daily") == 0) {
  866 + data->append("daily");
  867 + }
  868 + GXmlNode* daily = data->element("daily",0);
  869 +
  870 + // Loop over statistics
  871 + for (int i = 1; i < statistics.nrows(); ++i) {
  872 +
  873 + // Extract relevant attributes
  874 + std::string date = statistics.string(i,0).substr(0,10);
  875 + std::string country = statistics.string(i,2);
  876 + std::string tool = statistics.string(i,3);
  877 + std::string version = statistics.string(i,4);
  878 + double wall = statistics.real(i,5);
  879 + double cpu = statistics.real(i,6);
  880 + double gCO2e = statistics.real(i,7);
  881 +
  882 + // If tool name is empty then set tool name to "unknown"
  883 + if (tool.empty()) {
  884 + tool = "unknown";
  885 + }
  886 +
  887 + // Get pointer to relevant "date" node. If no "date" node exists
611 888 // that corresponds to the date of the statistics then append an
612   - // element
613   - GXmlNode* n_date = NULL;
  889 + // "date" node for the required date.
  890 + GXmlNode* date_node = NULL;
614 891 int ndates = daily->elements("date");
615 892 if (ndates == 0) {
616   - n_date = daily->append("date");
617   - n_date->append(GXmlElement("value", date));
  893 + date_node = daily->append("date");
  894 + date_node->append(GXmlElement("value", date));
618 895 }
619 896 else {
620 897 for (int k = 0; k < ndates; ++k) {
621   - GXmlElement* n = daily->element("date", k);
622   - if (daily->element("date", k)->element("value",0)->value() == date) {
623   - n_date = n;
  898 + GXmlElement* element = daily->element("date", k);
  899 + if (element->element("value",0)->value() == date) {
  900 + date_node = element;
624 901 break;
625 902 }
626 903 }
627   - if (n_date == NULL) {
628   - n_date = daily->append("date");
629   - n_date->append(GXmlElement("value", date));
  904 + if (date_node == NULL) {
  905 + date_node = daily->append("date");
  906 + date_node->append(GXmlElement("value", date));
630 907 }
631 908 }
632 909  
633 910 // Get pointer to "tools" node. If no such node exists then append
634 911 // one
635   - GXmlNode* n_tools = NULL;
636   - if (n_date->elements("tools") == 0) {
637   - n_tools = n_date->append("tools");
  912 + GXmlNode* tools = NULL;
  913 + if (date_node->elements("tools") == 0) {
  914 + tools = date_node->append("tools");
638 915 }
639 916 else {
640   - n_tools = n_date->element("tools",0);
  917 + tools = date_node->element("tools",0);
641 918 }
642 919  
643   - // Get pointer to relevant tool element. If no tool element exists
644   - // that corresponds to the relevant tool then append an element
645   - GXmlElement* n_tool = NULL;
646   - int ntools = n_tools->elements();
  920 + // Get pointer to relevant "tool" element. If no "tool" element
  921 + // exists that corresponds to the name of the tool then append
  922 + // a new element
  923 + GXmlElement* tool_element = NULL;
  924 + int ntools = tools->elements();
647 925 if (ntools == 0) {
648   - n_tool = n_tools->append(tool);
  926 + tool_element = tools->append(tool);
649 927 }
650 928 else {
651 929 for (int k = 0; k < ntools; ++k) {
652   - GXmlElement* n = n_tools->element(k);
653   - if (n->name() == tool) {
654   - n_tool = n;
  930 + GXmlElement* element = tools->element(k);
  931 + if (element->name() == tool) {
  932 + tool_element = element;
655 933 break;
656 934 }
657 935 }
658   - if (n_tool == NULL) {
659   - n_tool = n_tools->append(tool);
  936 + if (tool_element == NULL) {
  937 + tool_element = tools->append(tool);
660 938 }
661 939 }
662 940  
663   - // Set or update attributes
  941 + // Now we have the relevant "tool" element and we can update the
  942 + // statistics
664 943 int calls = 1;
665   - if (!n_tool->has_attribute("version")) {
666   - n_tool->attribute("version", version);
  944 + if (!tool_element->has_attribute("version")) {
  945 + tool_element->attribute("version", version);
667 946 }
668   - if (!n_tool->has_attribute("country")) {
669   - n_tool->attribute("country", country);
  947 + if (!tool_element->has_attribute("country")) {
  948 + tool_element->attribute("country", country);
670 949 }
671   - if (n_tool->has_attribute("calls")) {
672   - calls += gammalib::toint(n_tool->attribute("calls"));
  950 + if (tool_element->has_attribute("calls")) {
  951 + calls += gammalib::toint(tool_element->attribute("calls"));
673 952 }
674   - if (n_tool->has_attribute("wall")) {
675   - wall += gammalib::todouble(n_tool->attribute("wall"));
  953 + if (tool_element->has_attribute("wall")) {
  954 + wall += gammalib::todouble(tool_element->attribute("wall"));
676 955 }
677   - if (n_tool->has_attribute("cpu")) {
678   - cpu += gammalib::todouble(n_tool->attribute("cpu"));
  956 + if (tool_element->has_attribute("cpu")) {
  957 + cpu += gammalib::todouble(tool_element->attribute("cpu"));
679 958 }
680   - if (n_tool->has_attribute("gCO2e")) {
681   - gCO2e += gammalib::todouble(n_tool->attribute("gCO2e"));
  959 + if (tool_element->has_attribute("gCO2e")) {
  960 + gCO2e += gammalib::todouble(tool_element->attribute("gCO2e"));
682 961 }
683   - n_tool->attribute("calls", gammalib::str(calls));
684   - n_tool->attribute("wall", gammalib::str(wall));
685   - n_tool->attribute("cpu", gammalib::str(cpu));
686   - n_tool->attribute("gCO2e", gammalib::str(gCO2e));
  962 + tool_element->attribute("calls", gammalib::str(calls));
  963 + tool_element->attribute("wall", gammalib::str(wall));
  964 + tool_element->attribute("cpu", gammalib::str(cpu));
  965 + tool_element->attribute("gCO2e", gammalib::str(gCO2e));
687 966  
688 967 } // endfor: looped over statistics
689 968  
690   - // Update modified time in header
691   - GXmlNode* modified = header->element("dates > modified");
692   - static_cast<GXmlText*>((*modified)[0])->text(now.utc());
693   -
694   - // Update first and last time in header
695   - std::string start;
696   - std::string stop;
697   - if (statistics.nrows() > 1) {
698   - start = statistics.string(1,0);
699   - stop = statistics.string(statistics.nrows()-1,0);
700   - GXmlNode* n_start = header->element("dates > start");
701   - GXmlNode* n_stop = header->element("dates > stop");
702   - std::string current_start = static_cast<GXmlText*>((*n_start)[0])->text();
703   - std::string current_stop = static_cast<GXmlText*>((*n_stop)[0])->text();
704   - if (current_start.empty() || (start < current_start)) {
705   - static_cast<GXmlText*>((*n_start)[0])->text(start);
706   - }
707   - if (current_stop.empty() || (stop > current_stop)) {
708   - static_cast<GXmlText*>((*n_stop)[0])->text(stop);
709   - }
710   - }
711   -
712   - // Save high-level statistics file
713   - xml.save(filename);
714   -
715 969 // Return
716 970 return;
717 971 }
... ...