Ignore:
Timestamp:
2024-03-19T17:42:16+01:00 (8 months ago)
Author:
taylor.smock
Message:

reverter: Speed up reverting of large changesets where many objects have changed since the changeset was closed

This is done by using the OSM multi-fetch API to avoid doing a request per
version per object. Instead, we now do a bulk request for a series of versions
until we have all the information we need.

Additional performance enhancements include:

  • Don't do string concatenation prior to logging call for Reverting {target} to {old version}
  • reverter multi-fetch now queries 2k objects or 8k characters, whichever comes first
  • Some locations which fire DataSet events are now wrapped in DataSet#update calls
Location:
applications/editors/josm/plugins/reverter/src/reverter
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • applications/editors/josm/plugins/reverter/src/reverter/ChangesetReverter.java

    r36042 r36230  
    1515import java.util.List;
    1616import java.util.Map;
     17import java.util.Optional;
    1718import java.util.stream.Collectors;
    1819
     
    2223import org.openstreetmap.josm.data.conflict.Conflict;
    2324import org.openstreetmap.josm.data.coor.LatLon;
     25import org.openstreetmap.josm.data.osm.AbstractPrimitive;
    2426import org.openstreetmap.josm.data.osm.Changeset;
    2527import org.openstreetmap.josm.data.osm.ChangesetDataSet;
     
    273275            if (progressMonitor.isCanceled()) return;
    274276            nds = rdr.parseOsm(progressMonitor.createSubTaskMonitor(1, true));
    275             for (OsmPrimitive p : nds.allPrimitives()) {
    276                 if (!p.isIncomplete()) {
    277                     addMissingId(p);
    278                 } else {
    279                     if (ds.getPrimitiveById(p.getPrimitiveId()) == null) {
    280                         switch (p.getType()) {
    281                         case NODE: ds.addPrimitive(new Node(p.getUniqueId())); break;
    282                         case CLOSEDWAY:
    283                         case WAY: ds.addPrimitive(new Way(p.getUniqueId())); break;
    284                         case MULTIPOLYGON:
    285                         case RELATION: ds.addPrimitive(new Relation(p.getUniqueId())); break;
    286                         default: throw new AssertionError();
     277            ds.update(() -> {
     278                for (OsmPrimitive p : nds.allPrimitives()) {
     279                    if (!p.isIncomplete()) {
     280                        addMissingId(p);
     281                    } else {
     282                        if (ds.getPrimitiveById(p.getPrimitiveId()) == null) {
     283                            switch (p.getType()) {
     284                            case NODE: ds.addPrimitive(new Node(p.getUniqueId())); break;
     285                            case CLOSEDWAY:
     286                            case WAY: ds.addPrimitive(new Way(p.getUniqueId())); break;
     287                            case MULTIPOLYGON:
     288                            case RELATION: ds.addPrimitive(new Relation(p.getUniqueId())); break;
     289                            default: throw new AssertionError();
     290                            }
    287291                        }
    288292                    }
    289293                }
    290             }
     294            });
    291295        } finally {
    292296            progressMonitor.finishTask();
     
    503507        int num = nodes.size();
    504508        progressMonitor.beginTask(addChangesetIdPrefix(
    505                 trn("Checking coordinates of {0} node", "Checking coordinates of {0} nodes", num, num)), num);
    506 
     509                trn("Checking coordinates of {0} node", "Checking coordinates of {0} nodes", num, num)),
     510                // downloads == num ticks, then we get the downloaded data (1 tick), then we process the nodes (num ticks)
     511                2 * num + 1);
     512
     513        // Do bulk version fetches first
     514        // The objects to get next
     515        final Map<Long, Integer> versionMap = nodes.stream()
     516                .collect(Collectors.toMap(AbstractPrimitive::getUniqueId,
     517                        id -> Math.max(1, Optional.ofNullable(ds.getPrimitiveById(id)).orElse(id).getVersion() - 1)));
    507518        try {
    508             for (Node n : nodes) {
    509                 if (!n.isDeleted() && !n.isLatLonKnown()) {
    510                     PrimitiveId id = n.getPrimitiveId();
    511                     OsmPrimitive p = ds.getPrimitiveById(id);
    512                     if (p instanceof Node && !((Node) p).isLatLonKnown()) {
    513                         int version = p.getVersion();
    514                         while (version > 1) {
    515                             // find the version that was in use when the current changeset was closed
    516                             --version;
    517                             final OsmServerMultiObjectReader rdr = new OsmServerMultiObjectReader();
    518                             readObjectVersion(rdr, id, version, progressMonitor);
    519                             DataSet history = rdr.parseOsm(progressMonitor.createSubTaskMonitor(1, true));
    520                             if (!history.isEmpty()) {
    521                                 Node historyNode = (Node) history.allPrimitives().iterator().next();
    522                                 if (historyNode.isLatLonKnown() && changeset.getClosedAt().isAfter(historyNode.getInstant())) {
    523                                     n.load(historyNode.save());
    524                                     break;
    525                                 }
     519            while (!versionMap.isEmpty()) {
     520                final OsmServerMultiObjectReader rds = new OsmServerMultiObjectReader();
     521                final ProgressMonitor subMonitor = progressMonitor.createSubTaskMonitor(num, false);
     522                subMonitor.beginTask(tr("Fetching multi-objects"), versionMap.size());
     523                rds.readMultiObjects(OsmPrimitiveType.NODE, versionMap, subMonitor);
     524                subMonitor.finishTask();
     525                final DataSet history = rds.parseOsm(progressMonitor.createSubTaskMonitor(0, false));
     526                versionMap.replaceAll((key, value) -> value - 1);
     527                versionMap.values().removeIf(i -> i <= 0);
     528                ds.update(() -> {
     529                    for (Node n : nodes) {
     530                        if (!n.isDeleted() && !n.isLatLonKnown()) {
     531                            final Node historyNode = (Node) history.getPrimitiveById(n);
     532                            if (historyNode != null && historyNode.isLatLonKnown()
     533                                    && changeset.getClosedAt().isAfter(historyNode.getInstant())) {
     534                                n.load(historyNode.save());
     535                                versionMap.remove(n.getUniqueId());
     536                                progressMonitor.worked(1);
    526537                            }
    527538                        }
     539                        if (progressMonitor.isCanceled()) {
     540                            break;
     541                        }
    528542                    }
     543                });
     544                if (progressMonitor.isCanceled()) {
     545                    break;
    529546                }
    530                 if (progressMonitor.isCanceled())
    531                     return;
    532                 progressMonitor.worked(1);
    533547            }
    534548        } finally {
  • applications/editors/josm/plugins/reverter/src/reverter/DataSetCommandMerger.java

    r35464 r36230  
    77import java.util.Collection;
    88import java.util.HashMap;
     9import java.util.HashSet;
    910import java.util.LinkedHashSet;
    1011import java.util.LinkedList;
    1112import java.util.List;
    1213import java.util.Map;
     14import java.util.Set;
    1315
    1416import org.openstreetmap.josm.command.ChangeCommand;
     
    3840    private final DataSet targetDataSet;
    3941
    40     private final List<Command> cmds = new LinkedList<>();
    41     private final List<OsmPrimitive> nominalRevertedPrimitives = new LinkedList<>();
     42    private final List<Command> cmds = new ArrayList<>();
     43    private final Set<OsmPrimitive> nominalRevertedPrimitives = new HashSet<>();
    4244
    4345    /**
     
    6062                nominalRevertedPrimitives.add(target);
    6163            }
    62             Logging.debug("Reverting " + target + " to " + newTarget);
     64            Logging.debug("Reverting {0} to {1}", target, newTarget);
    6365        }
    6466    }
     
    126128            // Target node has been deleted by a more recent changeset -> conflict
    127129            } else if (sourceNode.isIncomplete() && !conflicts.hasConflictForMy(targetNode)) {
    128                 localConflicts.add(new Conflict<OsmPrimitive>(targetNode, sourceNode, true));
     130                localConflicts.add(new Conflict<>(targetNode, sourceNode, true));
    129131            } else {
    130132               Logging.info("Skipping target node "+targetNode+" for source node "+sourceNode+" while reverting way "+source);
  • applications/editors/josm/plugins/reverter/src/reverter/OsmServerMultiObjectReader.java

    r35638 r36230  
    4343        }
    4444    }
     45
     46    /**
     47     * Generate query strings
     48     * @param type The type of the object to get
     49     * @param list The map of ids to versions
     50     * @return The queries to make
     51     */
    4552    private static List<String> makeQueryStrings(OsmPrimitiveType type, Map<Long,Integer> list) {
    46         List<String> result = new ArrayList<>((list.size()+maxQueryIds-1)/maxQueryIds);
     53        // This is a "worst-case" calculation. Keep it fast (and err higher rather than lower), not accurate.
     54        final int expectedSize = (int) (list.entrySet().stream().mapToLong(entry ->
     55                // Keep in mind that 0-3 is 0, 3-32 is 1, 32-316 is 2, and so on when rounding log10.
     56                // First the key.
     57                Math.round(Math.log10(entry.getKey())) + 1 +
     58                // Then the value
     59                Math.round(Math.log10(entry.getValue())) + 1 +
     60                // And finally the "static" size (',' + 'v')
     61                2
     62                ).sum() / MAX_QUERY_LENGTH);
     63        List<String> result = new ArrayList<>(expectedSize + 1);
    4764        StringBuilder sb = new StringBuilder();
    4865        int cnt=0;
     
    6077            sb.append(entry.getValue());
    6178            cnt++;
    62             if (cnt >=maxQueryIds) {
     79            if (cnt >= MAX_QUERY_IDS || sb.length() > MAX_QUERY_LENGTH) {
    6380                result.add(sb.toString());
    6481                sb.setLength(0);
     
    7289    }
    7390
    74     protected static final int maxQueryIds = 128;
     91    /**
     92     * The maximum ids we want to query. API docs indicate 725 is "safe" for non-versioned objects with 10-digit ids.
     93     * Since we use {@link #MAX_QUERY_LENGTH} to avoid issues where we go over the permitted length, we can have a
     94     * bigger number here.
     95     * @see <a href="https://wiki.openstreetmap.org/wiki/API_v0.6#Multi_fetch:_GET_/api/0.6/[nodes|ways|relations]?#parameters">
     96     *     API_v0.6#Multi_fetch
     97     * </a>
     98     */
     99    protected static final int MAX_QUERY_IDS = 2000;
     100    /**
     101     * The maximum query length. Docs indicate 8213 characters in the URI is safe. The maximum base length before
     102     * query parameters is for relations at 59 characters for 8154 characters in the query parameters. We round down to
     103     * 8000 characters.
     104     * @see <a href="https://wiki.openstreetmap.org/wiki/API_v0.6#Multi_fetch:_GET_/api/0.6/[nodes|ways|relations]?#parameters">
     105     *     API_v0.6#Multi_fetch
     106     * </a>
     107     */
     108    protected static final int MAX_QUERY_LENGTH = 8000;
     109
     110    /**
     111     * Parse many objects
     112     * @param type The object type (<i>must</i> be common between all objects)
     113     * @param list The map of object id to object version
     114     * @param progressMonitor The progress monitor to update
     115     * @throws OsmTransferException If there is an issue getting the data
     116     */
    75117    public void readMultiObjects(OsmPrimitiveType type, Map<Long,Integer> list, ProgressMonitor progressMonitor) throws OsmTransferException {
    76118        for (String query : makeQueryStrings(type,list)) {
  • applications/editors/josm/plugins/reverter/src/reverter/RevertChangesetTask.java

    r36042 r36230  
    191191            return null;
    192192        }
    193         GuiHelper.runInEDT(() -> {
     193        GuiHelper.runInEDT(() -> cmds.get(0).getAffectedDataSet().update(() -> {
    194194            for (Command c : cmds) {
    195195                if (c instanceof ConflictAddCommand) {
     
    198198                c.executeCommand();
    199199            }
    200         });
     200        }));
    201201        final String desc;
    202202        if (revertType == RevertType.FULL) {
Note: See TracChangeset for help on using the changeset viewer.