From ee342bd65488b50debdce61ec6c7c1d435674929 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 18 Nov 2023 10:50:46 -0500 Subject: [PATCH 01/81] lua: use non-member functions for interop Currently, Tilemaker uses member functions for interop: ```lua function node_function(node) node:Layer(...) ``` This PR changes Tilemaker to use global functions: ```lua function node_function() Layer(...) ``` The chief rationale is performance. Every member function call needs to push an extra pointer onto the stack when crossing the Lua/C++ boundary. Kaguya serializes this pointer as a Lua userdata. That means every call into Lua has to malloc some memory, and every call back from Lua has to dereference through this pointer. And there are a lot of calls! For OMT on the GB extract, I counted ~1.4B calls from Lua into C++. A secondary rationale is that a global function is a bit more honest. A user might believe that this is currently permissible: ```lua last_node = nil function node_function(node) if last_node ~= nil -- do something with last_node end -- save the current node for later, for some reason last_node = node ``` But in reality, the OSM objects we pass into Lua don't behave quite like Lua objects. They're backed by OsmLuaProcessing, who will move on, invalidating whatever the user thinks they've got a reference to. This PR has a noticeable decrease in reading time for me, measured on the OMT profile for GB, on a 16-core computer: Before: ``` real 1m28.230s user 19m30.281s sys 0m29.610s ``` After: ``` real 1m21.728s user 17m27.150s sys 0m32.668s ``` The tradeoffs: - anyone with a custom Lua profile will need to update it, although the changes are fairly mechanical - Tilemaker now reserves several functions in the global namespace, causing the potential for conflicts --- docs/CONFIGURATION.md | 51 +-- docs/RELATIONS.md | 34 +- resources/process-debug.lua | 324 ++++++++++--------- resources/process-example.lua | 34 +- resources/process-openmaptiles.lua | 502 ++++++++++++++--------------- resources/process_coastline.lua | 4 +- src/osm_lua_processing.cpp | 91 ++++-- 7 files changed, 544 insertions(+), 496 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d41fba9b..d605d153 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -107,13 +107,16 @@ For example: ### Lua processing -Your Lua file needs to supply 5 things: +Your Lua file needs to supply a few things: 1. `node_keys`, a list of those OSM keys which indicate that a node should be processed -2. `init_function(name)` (optional), a function to initialize Lua logic -2. `node_function(node)`, a function to process an OSM node and add it to layers -3. `way_function(way)`, a function to process an OSM way and add it to layers -3. `exit_function` (optional), a function to finalize Lua logic (useful to show statistics) +2. `node_function()`, a function to process an OSM node and add it to layers +3. `way_function()`, a function to process an OSM way and add it to layers +4. (optional) `init_function(name)`, a function to initialize Lua logic +5. (optional) `exit_function`, a function to finalize Lua logic (useful to show statistics) +6. (optional) `relation_scan_function`, a function to determine whether your Lua file wishes to process the given relation +7. (optional) `relation_function`, a function to process an OSM relation and add it to layers +8. (optional) `attribute_function`, a function to remap attributes from shapefiles `node_keys` is a simple list (or in Lua parlance, a 'table') of OSM tag keys. If a node has one of those keys, it will be processed by `node_function`; if not, it'll be skipped. For example, if you wanted to show highway crossings and railway stations, it should be `{ "highway", "railway" }`. (This avoids the need to process the vast majority of nodes which contain no important tags at all.) @@ -127,28 +130,30 @@ Note the order: you write to a layer first, then set attributes after. To do that, you use these methods: -* `node:Find(key)` or `way:Find(key)`: get the value for a tag, or the empty string if not present. For example, `way:Find("railway")` might return "rail" for a railway, "siding" for a siding, or "" if it isn't a railway at all. -* `node:Holds(key)` or `way:Holds(key)`: returns true if that key exists, false otherwise. -* `node:Layer("layer_name", false)` or `way:Layer("layer_name", is_area)`: write this node/way to the named layer. This is how you put objects in your vector tile. is_area (true/false) specifies whether a way should be treated as an area, or just as a linestring. -* `way:LayerAsCentroid("layer_name")`: write a single centroid point for this way to the named layer (useful for labels and POIs). -* `node:Attribute(key,value,minzoom)` or `node:Attribute(key,value,minzoom)`: add an attribute to the most recently written layer. Argument `minzoom` is optional, use it if you do not want to write the attribute on lower zoom levels. -* `node:AttributeNumeric(key,value,minzoom)`, `node:AttributeBoolean(key,value,minzoom)` (and `way:`...): for numeric/boolean columns. -* `node:Id()` or `way:Id()`: get the OSM ID of the current object. -* `node:ZOrder(number)` or `way:ZOrder(number)`: Set a numeric value (default 0, 1-byte signed integer) used to sort features within a layer. Use this feature to ensure a proper rendering order if the rendering engine itself does not support sorting. Sorting is not supported across layers merged with `write_to`. Features with different z-order are not merged if `combine_below` or `combine_polygons_below` is used. -* `node:MinZoom(zoom)` or `way:MinZoom(zoom)`: set the minimum zoom level (0-15) at which this object will be written. Note that the JSON layer configuration minimum still applies (so `:MinZoom(5)` will have no effect if your layer only starts at z6). -* `way:Length()` and `way:Area()`: return the length (metres)/area (square metres) of the current object. Requires recent Boost. -* `way:Centroid()`: return the lat/lon of the centre of the current object as a two-element Lua table (element 1 is lat, 2 is lon). +* `Find(key)`: get the value for a tag, or the empty string if not present. For example, `Find("railway")` might return "rail" for a railway, "siding" for a siding, or "" if it isn't a railway at all. +* `Holds(key)`: returns true if that key exists, false otherwise. +* `Layer("layer_name", is_area)`: write this node/way to the named layer. This is how you put objects in your vector tile. is_area (true/false) specifies whether a way should be treated as an area, or just as a linestring. +* `LayerAsCentroid("layer_name")`: write a single centroid point for this way to the named layer (useful for labels and POIs). +* `Attribute(key,value,minzoom)`: add an attribute to the most recently written layer. Argument `minzoom` is optional, use it if you do not want to write the attribute on lower zoom levels. +* `AttributeNumeric(key,value,minzoom)`, `AttributeBoolean(key,value,minzoom)`: for numeric/boolean columns. +* `Id()`: get the OSM ID of the current object. +* `ZOrder(number)`: Set a numeric value (default 0, 1-byte signed integer) used to sort features within a layer. Use this feature to ensure a proper rendering order if the rendering engine itself does not support sorting. Sorting is not supported across layers merged with `write_to`. Features with different z-order are not merged if `combine_below` or `combine_polygons_below` is used. +* `MinZoom(zoom)`: set the minimum zoom level (0-15) at which this object will be written. Note that the JSON layer configuration minimum still applies (so `:MinZoom(5)` will have no effect if your layer only starts at z6). +* `Length()` and `Area()`: return the length (metres)/area (square metres) of the current object. Requires recent Boost. +* `Centroid()`: return the lat/lon of the centre of the current object as a two-element Lua table (element 1 is lat, 2 is lon). The simplest possible function, to include roads/paths and nothing else, might look like this: - function way_function(way) - local highway = way:Find("highway") +```lua + function way_function() + local highway = Find("highway") if highway~="" then - way:Layer("roads", false) - way:Attribute("name", way:Find("name")) - way:Attribute("type", highway) + Layer("roads", false) + Attribute("name", Find("name")) + Attribute("type", highway) end end +``` Take a look at the supplied process.lua for a simple example, or the more complex OpenMapTiles-compatible script in `resources/`. You can specify another filename with the `--process` option. @@ -197,11 +202,11 @@ When processing OSM objects with your Lua script, you can perform simple spatial You can then find out whether a node is within one of these polygons using the `Intersects` method: - if node:Intersects("countries") then print("Looks like it's on land"); end + if Intersects("countries") then print("Looks like it's on land"); end Or you can find out what country(/ies) the node is within using `FindIntersecting`, which returns a table: - names = node:FindIntersecting("countries") + names = FindIntersecting("countries") print(table.concat(name,",")) To enable these functions, set `index` to true in your shapefile layer definition. `index_column` is not needed for `Intersects` but required for `FindIntersecting`. diff --git a/docs/RELATIONS.md b/docs/RELATIONS.md index 6e436b68..6fc3b557 100644 --- a/docs/RELATIONS.md +++ b/docs/RELATIONS.md @@ -22,26 +22,30 @@ This is a two-stage process: first, when reading relations, indicate that these To define which relations should be accepted, add a `relation_scan_function`: - function relation_scan_function(relation) - if relation:Find("type")=="route" and relation:Find("route")=="bicycle" then - local network = relation:Find("network") - if network=="ncn" then relation:Accept() end +```lua + function relation_scan_function() + if Find("type")=="route" and Find("route")=="bicycle" then + local network = Find("network") + if network=="ncn" then Accept() end end end +``` -This function takes the relation as its sole argument. Examine the tags using `relation:Find(key)` as normal. (You can also use `relation:Holds(key)` and `relation:Id()`.) If you want to use this relation, call `relation:Accept()`. +Examine the tags using `Find(key)` as normal. (You can also use `Holds(key)` and `Id()`.) If you want to use this relation, call `Accept()`. #### Stage 2: accessing relations from ways -Now that you've accepted the relations, they will be available from `way_function`. They are accessed using an iterator (`way:NextRelation()`) which reads each relation for that way in turn, returning nil when there are no more relations available. Once you have accessed a relation with the iterator, you can read its tags with `way:FindInRelation(key)`. For example: +Now that you've accepted the relations, they will be available from `way_function`. They are accessed using an iterator (`NextRelation()`) which reads each relation for that way in turn, returning nil when there are no more relations available. Once you have accessed a relation with the iterator, you can read its tags with `FindInRelation(key)`. For example: +```lua while true do - local rel = way:NextRelation() + local rel = NextRelation() if not rel then break end - print ("Part of route "..way:FindInRelation("ref")) + print ("Part of route "..FindInRelation("ref")) end +``` -(Should you need to re-read the relations, you can reset the iterator with `way:RestartRelations()`.) +(Should you need to re-read the relations, you can reset the iterator with `RestartRelations()`.) ### Writing relation geometries @@ -52,13 +56,15 @@ First, make sure that you have accepted the relations using `relation_scan_funct Then write a `relation_function`, which works in the same way as `way_function` would: - function relation_function(relation) - if relation:Find("type")=="route" and relation:Find("route")=="bicycle" then - relation:Layer("bike_routes", false) - relation:Attribute("class", relation:Find("network")) - relation:Attribute("ref", relation:Find("ref")) +```lua + function relation_function() + if Find("type")=="route" and Find("route")=="bicycle" then + Layer("bike_routes", false) + Attribute("class", Find("network")) + Attribute("ref", Find("ref")) end end +``` ### Not supported diff --git a/resources/process-debug.lua b/resources/process-debug.lua index ea594c19..e1c8e62f 100644 --- a/resources/process-debug.lua +++ b/resources/process-debug.lua @@ -45,36 +45,36 @@ aerodromeValues = Set { "international", "public", "regional", "military", "priv -- Process node tags node_keys = { "amenity", "shop", "sport", "tourism", "place", "office", "natural", "addr:housenumber", "aeroway" } -function node_function(node) +function node_function() -- Write 'aerodrome_label' - local aeroway = node:Find("aeroway") + local aeroway = Find("aeroway") if aeroway == "aerodrome" then - node:Layer("aerodrome_label", false) - SetNameAttributes(node) - node:Attribute("iata", node:Find("iata")) - SetEleAttributes(node) - node:Attribute("icao", node:Find("icao")) + Layer("aerodrome_label", false) + SetNameAttributes() + Attribute("iata", Find("iata")) + SetEleAttributes() + Attribute("icao", Find("icao")) - local aerodrome_value = node:Find("aerodrome") + local aerodrome_value = Find("aerodrome") local class if aerodromeValues[aerodrome_value] then class = aerodrome_value else class = "other" end - node:Attribute("class", class) + Attribute("class", class) end -- Write 'housenumber' - local housenumber = node:Find("addr:housenumber") + local housenumber = Find("addr:housenumber") if housenumber~="" then - node:Layer("housenumber", false) - node:Attribute("housenumber", housenumber) + Layer("housenumber", false) + Attribute("housenumber", housenumber) end -- Write 'place' -- note that OpenMapTiles has a rank for countries (1-3), states (1-6) and cities (1-10+); -- we could potentially approximate it for cities based on the population tag - local place = node:Find("place") + local place = Find("place") if place ~= "" then local rank = nil local mz = 13 - local pop = tonumber(node:Find("population")) or 0 + local pop = tonumber(Find("population")) or 0 if place == "continent" then mz=2 elseif place == "country" then mz=3; rank=1 @@ -90,31 +90,31 @@ function node_function(node) elseif place == "locality" then mz=13 end - node:Layer("place", false) - node:Attribute("class", place) - node:MinZoom(mz) - if rank then node:AttributeNumeric("rank", rank) end - SetNameAttributes(node) + Layer("place", false) + Attribute("class", place) + MinZoom(mz) + if rank then AttributeNumeric("rank", rank) end + SetNameAttributes() return end -- Write 'poi' - local rank, class, subclass = GetPOIRank(node) + local rank, class, subclass = GetPOIRank() if rank then WritePOI(node,class,subclass,rank) end -- Write 'mountain_peak' and 'water_name' - local natural = node:Find("natural") + local natural = Find("natural") if natural == "peak" or natural == "volcano" then - node:Layer("mountain_peak", false) - SetEleAttributes(node) - node:AttributeNumeric("rank", 1) - node:Attribute("class", natural) - SetNameAttributes(node) + Layer("mountain_peak", false) + SetEleAttributes() + AttributeNumeric("rank", 1) + Attribute("class", natural) + SetNameAttributes() return end if natural == "bay" then - node:Layer("water_name", false) - SetNameAttributes(node) + Layer("water_name", false) + SetNameAttributes() return end end @@ -196,33 +196,33 @@ waterClasses = Set { "river", "riverbank", "stream", "canal", "drain", "ditch waterwayClasses = Set { "stream", "river", "canal", "drain", "ditch" } -function way_function(way) - local highway = way:Find("highway") - local waterway = way:Find("waterway") - local water = way:Find("water") - local building = way:Find("building") - local natural = way:Find("natural") - local historic = way:Find("historic") - local landuse = way:Find("landuse") - local leisure = way:Find("leisure") - local amenity = way:Find("amenity") - local aeroway = way:Find("aeroway") - local railway = way:Find("railway") - local sport = way:Find("sport") - local shop = way:Find("shop") - local tourism = way:Find("tourism") - local man_made = way:Find("man_made") - local isClosed = way:IsClosed() - local housenumber = way:Find("addr:housenumber") +function way_function() + local highway = Find("highway") + local waterway = Find("waterway") + local water = Find("water") + local building = Find("building") + local natural = Find("natural") + local historic = Find("historic") + local landuse = Find("landuse") + local leisure = Find("leisure") + local amenity = Find("amenity") + local aeroway = Find("aeroway") + local railway = Find("railway") + local sport = Find("sport") + local shop = Find("shop") + local tourism = Find("tourism") + local man_made = Find("man_made") + local isClosed = IsClosed() + local housenumber = Find("addr:housenumber") local write_name = false - local construction = way:Find("construction") + local construction = Find("construction") -- Miscellaneous preprocessing - if way:Find("disused") == "yes" then return end + if Find("disused") == "yes" then return end if highway == "proposed" then return end if aerowayBuildings[aeroway] then building="yes"; aeroway="" end if landuse == "field" then landuse = "farmland" end - if landuse == "meadow" and way:Find("meadow")=="agricultural" then landuse="farmland" end + if landuse == "meadow" and Find("meadow")=="agricultural" then landuse="farmland" end -- Roads ('transportation' and 'transportation_name', plus 'transportation_name_detail') if highway~="" then @@ -235,33 +235,33 @@ function way_function(way) if trackValues[highway] then h = "track"; layer="transportation_detail" end if pathValues[highway] then h = "path" ; layer="transportation_detail" end if h=="service" then layer="transportation_detail" end - way:Layer(layer, false) - way:Attribute("class", h) - SetBrunnelAttributes(way) + Layer(layer, false) + Attribute("class", h) + SetBrunnelAttributes() -- Construction if highway == "construction" then if constructionValues[construction] then - way:Attribute("class", construction .. "_construction") + Attribute("class", construction .. "_construction") else - way:Attribute("class", "minor_construction") + Attribute("class", "minor_construction") end end -- Service - local service = way:Find("service") - if highway == "service" and service ~="" then way:Attribute("service", service) end + local service = Find("service") + if highway == "service" and service ~="" then Attribute("service", service) end -- Links (ramp) if linkValues[highway] then splitHighway = split(highway, "_") highway = splitHighway[1] - way:AttributeNumeric("ramp",1) + AttributeNumeric("ramp",1) end - local oneway = way:Find("oneway") + local oneway = Find("oneway") if oneway == "yes" or oneway == "1" then - way:AttributeNumeric("oneway",1) + AttributeNumeric("oneway",1) end if oneway == "-1" then -- **** TODO @@ -269,115 +269,115 @@ function way_function(way) -- Write names if layer == "motorway" or layer == "trunk" then - way:Layer("transportation_name", false) + Layer("transportation_name", false) elseif h == "minor" or h == "track" or h == "path" or h == "service" then - way:Layer("transportation_name_detail", false) + Layer("transportation_name_detail", false) else - way:Layer("transportation_name_mid", false) + Layer("transportation_name_mid", false) end - SetNameAttributes(way) - way:Attribute("class",h) - way:Attribute("network","road") -- **** needs fixing - if h~=highway then way:Attribute("subclass",highway) end - local ref = way:Find("ref") + SetNameAttributes() + Attribute("class",h) + Attribute("network","road") -- **** needs fixing + if h~=highway then Attribute("subclass",highway) end + local ref = Find("ref") if ref~="" then - way:Attribute("ref",ref) - way:AttributeNumeric("ref_length",ref:len()) + Attribute("ref",ref) + AttributeNumeric("ref_length",ref:len()) end end -- Railways ('transportation' and 'transportation_name', plus 'transportation_name_detail') if railway~="" then - way:Layer("transportation", false) - way:Attribute("class", railway) + Layer("transportation", false) + Attribute("class", railway) - way:Layer("transportation_name", false) - SetNameAttributes(way) - way:MinZoom(14) - way:Attribute("class", "rail") + Layer("transportation_name", false) + SetNameAttributes() + MinZoom(14) + Attribute("class", "rail") end -- 'Aeroway' if aeroway~="" then - way:Layer("aeroway", isClosed) - way:Attribute("class",aeroway) - way:Attribute("ref",way:Find("ref")) + Layer("aeroway", isClosed) + Attribute("class",aeroway) + Attribute("ref",Find("ref")) write_name = true end -- 'aerodrome_label' if aeroway=="aerodrome" then - way:LayerAsCentroid("aerodrome_label") - SetNameAttributes(way) - way:Attribute("iata", way:Find("iata")) - SetEleAttributes(way) - way:Attribute("icao", way:Find("icao")) + LayerAsCentroid("aerodrome_label") + SetNameAttributes() + Attribute("iata", Find("iata")) + SetEleAttributes() + Attribute("icao", Find("icao")) - local aerodrome = way:Find(aeroway) + local aerodrome = Find(aeroway) local class if aerodromeValues[aerodrome] then class = aerodrome else class = "other" end - way:Attribute("class", class) + Attribute("class", class) end -- Set 'waterway' and associated if waterwayClasses[waterway] and not isClosed then - if waterway == "river" and way:Holds("name") then - way:Layer("waterway", false) + if waterway == "river" and Holds("name") then + Layer("waterway", false) else - way:Layer("waterway_detail", false) + Layer("waterway_detail", false) end - if way:Find("intermittent")=="yes" then way:AttributeNumeric("intermittent", 1) else way:AttributeNumeric("intermittent", 0) end - way:Attribute("class", waterway) - SetNameAttributes(way) - SetBrunnelAttributes(way) - elseif waterway == "boatyard" then way:Layer("landuse", isClosed); way:Attribute("class", "industrial") - elseif waterway == "dam" then way:Layer("building",isClosed) - elseif waterway == "fuel" then way:Layer("landuse", isClosed); way:Attribute("class", "industrial") + if Find("intermittent")=="yes" then AttributeNumeric("intermittent", 1) else AttributeNumeric("intermittent", 0) end + Attribute("class", waterway) + SetNameAttributes() + SetBrunnelAttributes() + elseif waterway == "boatyard" then Layer("landuse", isClosed); Attribute("class", "industrial") + elseif waterway == "dam" then Layer("building",isClosed) + elseif waterway == "fuel" then Layer("landuse", isClosed); Attribute("class", "industrial") end -- Set names on rivers if waterwayClasses[waterway] and not isClosed then - if waterway == "river" and way:Holds("name") then - way:Layer("water_name", false) + if waterway == "river" and Holds("name") then + Layer("water_name", false) else - way:Layer("water_name_detail", false) - way:MinZoom(14) + Layer("water_name_detail", false) + MinZoom(14) end - way:Attribute("class", waterway) - SetNameAttributes(way) + Attribute("class", waterway) + SetNameAttributes() end -- Set 'building' and associated if building~="" then - way:Layer("building", true) - SetMinZoomByArea(way) + Layer("building", true) + SetMinZoomByArea() end -- Set 'housenumber' if housenumber~="" then - way:LayerAsCentroid("housenumber", false) - way:Attribute("housenumber", housenumber) + LayerAsCentroid("housenumber", false) + Attribute("housenumber", housenumber) end -- Set 'water' if natural=="water" or natural=="bay" or leisure=="swimming_pool" or landuse=="reservoir" or landuse=="basin" or waterClasses[waterway] then - if way:Find("covered")=="yes" or not isClosed then return end + if Find("covered")=="yes" or not isClosed then return end local class="lake"; if natural=="bay" then class="ocean" elseif waterway~="" then class="river" end - way:Layer("water",true) --- SetMinZoomByArea(way) - way:Attribute("class",class) + Layer("water",true) +-- SetMinZoomByArea() + Attribute("class",class) - if way:Find("intermittent")=="yes" then way:Attribute("intermittent",1) end + if Find("intermittent")=="yes" then Attribute("intermittent",1) end -- we only want to show the names of actual lakes not every man-made basin that probably doesn't even have a name other than "basin" -- examples for which we don't want to show a name: -- https://www.openstreetmap.org/way/25958687 -- https://www.openstreetmap.org/way/27201902 -- https://www.openstreetmap.org/way/25309134 -- https://www.openstreetmap.org/way/24579306 - if way:Holds("name") and natural=="water" and water ~= "basin" and water ~= "wastewater" then - way:LayerAsCentroid("water_name_detail") - SetNameAttributes(way) --- SetMinZoomByArea(way) - way:Attribute("class", class) + if Holds("name") and natural=="water" and water ~= "basin" and water ~= "wastewater" then + LayerAsCentroid("water_name_detail") + SetNameAttributes() +-- SetMinZoomByArea() + Attribute("class", class) end return -- in case we get any landuse processing @@ -388,11 +388,11 @@ function way_function(way) if l=="" then l=natural end if l=="" then l=leisure end if landcoverKeys[l] then - way:Layer("landcover", true) - SetMinZoomByArea(way) - way:Attribute("class", landcoverKeys[l]) - if l=="wetland" then way:Attribute("subclass", way:Find("wetland")) - else way:Attribute("subclass", l) end + Layer("landcover", true) + SetMinZoomByArea() + Attribute("class", landcoverKeys[l]) + if l=="wetland" then Attribute("subclass", Find("wetland")) + else Attribute("subclass", l) end write_name = true -- Set 'landuse' @@ -400,26 +400,26 @@ function way_function(way) if l=="" then l=amenity end if l=="" then l=tourism end if landuseKeys[l] then - way:Layer("landuse", true) - way:Attribute("class", l) + Layer("landuse", true) + Attribute("class", l) write_name = true end end -- Parks - if boundary=="national_park" then way:Layer("park",true); way:Attribute("class",boundary); SetNameAttributes(way) - elseif leisure=="nature_reserve" then way:Layer("park",true); way:Attribute("class",leisure ); SetNameAttributes(way) end + if boundary=="national_park" then Layer("park",true); Attribute("class",boundary); SetNameAttributes() + elseif leisure=="nature_reserve" then Layer("park",true); Attribute("class",leisure ); SetNameAttributes() end -- POIs ('poi' and 'poi_detail') - local rank, class, subclass = GetPOIRank(way) + local rank, class, subclass = GetPOIRank() if rank then WritePOI(way,class,subclass,rank); return end -- Catch-all - if (building~="" or write_name) and way:Holds("name") then - way:LayerAsCentroid("poi_detail") - SetNameAttributes(way) + if (building~="" or write_name) and Holds("name") then + LayerAsCentroid("poi_detail") + SetNameAttributes() if write_name then rank=6 else rank=25 end - way:AttributeNumeric("rank", rank) + AttributeNumeric("rank", rank) end end @@ -435,65 +435,67 @@ end function WritePOI(obj,class,subclass,rank) local layer = "poi" if rank>4 then layer="poi_detail" end - obj:LayerAsCentroid(layer) + LayerAsCentroid(layer) SetNameAttributes(obj) - obj:AttributeNumeric("rank", rank) - obj:Attribute("class", class) - obj:Attribute("subclass", subclass) + AttributeNumeric("rank", rank) + Attribute("class", class) + Attribute("subclass", subclass) end -- Set name attributes on any object function SetNameAttributes(obj) - local name = obj:Find("name"), main_written = name, iname + local name = Find("name") + local main_written = name + local iname -- if we have a preferred language, then write that (if available), and additionally write the base name tag - if preferred_language and obj:Holds("name:"..preferred_language) then - iname = obj:Find("name:"..preferred_language) + if preferred_language and Holds("name:"..preferred_language) then + iname = Find("name:"..preferred_language) print("Found "..preferred_language..": "..iname) - obj:Attribute(preferred_language_attribute, iname) + Attribute(preferred_language_attribute, iname) if iname~=name and default_language_attribute then - obj:Attribute(default_language_attribute, name) + Attribute(default_language_attribute, name) else main_written = iname end else - obj:Attribute(preferred_language_attribute, name) + Attribute(preferred_language_attribute, name) end -- then set any additional languages for i,lang in ipairs(additional_languages) do - iname = obj:Find("name:"..lang) + iname = Find("name:"..lang) if iname=="" then iname=name end - if iname~=main_written then obj:Attribute("name:"..lang, iname) end + if iname~=main_written then Attribute("name:"..lang, iname) end end end -- Set ele and ele_ft on any object function SetEleAttributes(obj) - local ele = obj:Find("ele") + local ele = Find("ele") if ele ~= "" then local meter = math.floor(tonumber(ele) or 0) local feet = math.floor(meter * 3.2808399) - obj:AttributeNumeric("ele", meter) - obj:AttributeNumeric("ele_ft", feet) + AttributeNumeric("ele", meter) + AttributeNumeric("ele_ft", feet) end end function SetBrunnelAttributes(obj) - if obj:Find("bridge") == "yes" then obj:Attribute("brunnel", "bridge") - elseif obj:Find("tunnel") == "yes" then obj:Attribute("brunnel", "tunnel") - elseif obj:Find("ford") == "yes" then obj:Attribute("brunnel", "ford") + if Find("bridge") == "yes" then Attribute("brunnel", "bridge") + elseif Find("tunnel") == "yes" then Attribute("brunnel", "tunnel") + elseif Find("ford") == "yes" then Attribute("brunnel", "ford") end end -- Set minimum zoom level by area -function SetMinZoomByArea(way) - local area=way:Area() - if area>ZRES5^2 then way:MinZoom(6) - elseif area>ZRES6^2 then way:MinZoom(7) - elseif area>ZRES7^2 then way:MinZoom(8) - elseif area>ZRES8^2 then way:MinZoom(9) - elseif area>ZRES9^2 then way:MinZoom(10) - elseif area>ZRES10^2 then way:MinZoom(11) - elseif area>ZRES11^2 then way:MinZoom(12) - elseif area>ZRES12^2 then way:MinZoom(13) - else way:MinZoom(14) end +function SetMinZoomByArea() + local area=Area() + if area>ZRES5^2 then MinZoom(6) + elseif area>ZRES6^2 then MinZoom(7) + elseif area>ZRES7^2 then MinZoom(8) + elseif area>ZRES8^2 then MinZoom(9) + elseif area>ZRES9^2 then MinZoom(10) + elseif area>ZRES10^2 then MinZoom(11) + elseif area>ZRES11^2 then MinZoom(12) + elseif area>ZRES12^2 then MinZoom(13) + else MinZoom(14) end end -- Calculate POIs (typically rank 1-4 go to 'poi' z12-14, rank 5+ to 'poi_detail' z14) @@ -503,8 +505,8 @@ function GetPOIRank(obj) -- Can we find the tag? for k,list in pairs(poiTags) do - if list[obj:Find(k)] then - v = obj:Find(k) -- k/v are the OSM tag pair + if list[Find(k)] then + v = Find(k) -- k/v are the OSM tag pair class = poiClasses[v] or v rank = poiClassRanks[class] or 25 return rank, class, v @@ -512,7 +514,7 @@ function GetPOIRank(obj) end -- Catch-all for shops - local shop = obj:Find("shop") + local shop = Find("shop") if shop~="" then return poiClassRanks['shop'], "shop", shop end -- Nothing found diff --git a/resources/process-example.lua b/resources/process-example.lua index 41b461df..b4b1f108 100644 --- a/resources/process-example.lua +++ b/resources/process-example.lua @@ -14,33 +14,33 @@ end -- Assign nodes to a layer, and set attributes, based on OSM tags function node_function(node) - local amenity = node:Find("amenity") - local shop = node:Find("shop") + local amenity = Find("amenity") + local shop = Find("shop") if amenity~="" or shop~="" then - node:Layer("poi", false) - if amenity~="" then node:Attribute("class",amenity) - else node:Attribute("class",shop) end - node:Attribute("name", node:Find("name")) + Layer("poi", false) + if amenity~="" then Attribute("class",amenity) + else Attribute("class",shop) end + Attribute("name", Find("name")) end end -- Similarly for ways -function way_function(way) - local highway = way:Find("highway") - local waterway = way:Find("waterway") - local building = way:Find("building") +function way_function() + local highway = Find("highway") + local waterway = Find("waterway") + local building = Find("building") if highway~="" then - way:Layer("transportation", false) - way:Attribute("class", highway) --- way:Attribute("id",way:Id()) --- way:AttributeNumeric("area",37) + Layer("transportation", false) + Attribute("class", highway) +-- Attribute("id",Id()) +-- AttributeNumeric("area",37) end if waterway~="" then - way:Layer("waterway", false) - way:Attribute("class", waterway) + Layer("waterway", false) + Attribute("class", waterway) end if building~="" then - way:Layer("building", true) + Layer("building", true) end end diff --git a/resources/process-openmaptiles.lua b/resources/process-openmaptiles.lua index cbf8fadb..e58dd568 100644 --- a/resources/process-openmaptiles.lua +++ b/resources/process-openmaptiles.lua @@ -118,36 +118,36 @@ function calcRank(place, population, capital_al) end -function node_function(node) +function node_function() -- Write 'aerodrome_label' - local aeroway = node:Find("aeroway") + local aeroway = Find("aeroway") if aeroway == "aerodrome" then - node:Layer("aerodrome_label", false) - SetNameAttributes(node) - node:Attribute("iata", node:Find("iata")) - SetEleAttributes(node) - node:Attribute("icao", node:Find("icao")) + Layer("aerodrome_label", false) + SetNameAttributes() + Attribute("iata", Find("iata")) + SetEleAttributes() + Attribute("icao", Find("icao")) - local aerodrome_value = node:Find("aerodrome") + local aerodrome_value = Find("aerodrome") local class if aerodromeValues[aerodrome_value] then class = aerodrome_value else class = "other" end - node:Attribute("class", class) + Attribute("class", class) end -- Write 'housenumber' - local housenumber = node:Find("addr:housenumber") + local housenumber = Find("addr:housenumber") if housenumber~="" then - node:Layer("housenumber", false) - node:Attribute("housenumber", housenumber) + Layer("housenumber", false) + Attribute("housenumber", housenumber) end -- Write 'place' -- note that OpenMapTiles has a rank for countries (1-3), states (1-6) and cities (1-10+); -- we could potentially approximate it for cities based on the population tag - local place = node:Find("place") + local place = Find("place") if place ~= "" then local mz = 13 - local pop = tonumber(node:Find("population")) or 0 - local capital = capitalLevel(node:Find("capital")) + local pop = tonumber(Find("population")) or 0 + local capital = capitalLevel(Find("capital")) local rank = calcRank(place, pop, capital) if place == "continent" then mz=0 @@ -167,33 +167,33 @@ function node_function(node) elseif place == "locality" then mz=13 end - node:Layer("place", false) - node:Attribute("class", place) - node:MinZoom(mz) - if rank then node:AttributeNumeric("rank", rank) end - if capital then node:AttributeNumeric("capital", capital) end - if place=="country" then node:Attribute("iso_a2", node:Find("ISO3166-1:alpha2")) end - SetNameAttributes(node) + Layer("place", false) + Attribute("class", place) + MinZoom(mz) + if rank then AttributeNumeric("rank", rank) end + if capital then AttributeNumeric("capital", capital) end + if place=="country" then Attribute("iso_a2", Find("ISO3166-1:alpha2")) end + SetNameAttributes() return end -- Write 'poi' - local rank, class, subclass = GetPOIRank(node) - if rank then WritePOI(node,class,subclass,rank) end + local rank, class, subclass = GetPOIRank() + if rank then WritePOI(class,subclass,rank) end -- Write 'mountain_peak' and 'water_name' - local natural = node:Find("natural") + local natural = Find("natural") if natural == "peak" or natural == "volcano" then - node:Layer("mountain_peak", false) - SetEleAttributes(node) - node:AttributeNumeric("rank", 1) - node:Attribute("class", natural) - SetNameAttributes(node) + Layer("mountain_peak", false) + SetEleAttributes() + AttributeNumeric("rank", 1) + Attribute("class", natural) + SetNameAttributes() return end if natural == "bay" then - node:Layer("water_name", false) - SetNameAttributes(node) + Layer("water_name", false) + SetNameAttributes() return end end @@ -279,81 +279,81 @@ waterwayClasses = Set { "stream", "river", "canal", "drain", "ditch" } -- Scan relations for use in ways -function relation_scan_function(relation) - if relation:Find("type")=="boundary" and relation:Find("boundary")=="administrative" then - relation:Accept() +function relation_scan_function() + if Find("type")=="boundary" and Find("boundary")=="administrative" then + Accept() end end -function write_to_transportation_layer(way, minzoom, highway_class) - way:Layer("transportation", false) - way:MinZoom(minzoom) - SetZOrder(way) - way:Attribute("class", highway_class) - SetBrunnelAttributes(way) - if ramp then way:AttributeNumeric("ramp",1) end +function write_to_transportation_layer(minzoom, highway_class) + Layer("transportation", false) + MinZoom(minzoom) + SetZOrder() + Attribute("class", highway_class) + SetBrunnelAttributes() + if ramp then AttributeNumeric("ramp",1) end -- Service - if highway == "service" and service ~="" then way:Attribute("service", service) end + if highway == "service" and service ~="" then Attribute("service", service) end - local oneway = way:Find("oneway") + local oneway = Find("oneway") if oneway == "yes" or oneway == "1" then - way:AttributeNumeric("oneway",1) + AttributeNumeric("oneway",1) end if oneway == "-1" then -- **** TODO end - local surface = way:Find("surface") - local surfaceMinzoom = 12 + local surface = Find("surface") + local surfaceMinzoom = 12 if pavedValues[surface] then - way:Attribute("surface", "paved", surfaceMinzoom) + Attribute("surface", "paved", surfaceMinzoom) elseif unpavedValues[surface] then - way:Attribute("surface", "unpaved", surfaceMinzoom) - end - local accessMinzoom = 9 - if way:Holds("access") then way:Attribute("access", way:Find("access"), accessMinzoom) end - if way:Holds("bicycle") then way:Attribute("bicycle", way:Find("bicycle"), accessMinzoom) end - if way:Holds("foot") then way:Attribute("foot", way:Find("foot"), accessMinzoom) end - if way:Holds("horse") then way:Attribute("horse", way:Find("horse"), accessMinzoom) end - way:AttributeBoolean("toll", way:Find("toll") == "yes", accessMinzoom) - way:AttributeNumeric("layer", tonumber(way:Find("layer")) or 0, accessMinzoom) - way:AttributeBoolean("expressway", way:Find("expressway"), 7) - way:Attribute("mtb_scale", way:Find("mtb:scale"), 10) + Attribute("surface", "unpaved", surfaceMinzoom) + end + local accessMinzoom = 9 + if Holds("access") then Attribute("access", Find("access"), accessMinzoom) end + if Holds("bicycle") then Attribute("bicycle", Find("bicycle"), accessMinzoom) end + if Holds("foot") then Attribute("foot", Find("foot"), accessMinzoom) end + if Holds("horse") then Attribute("horse", Find("horse"), accessMinzoom) end + AttributeBoolean("toll", Find("toll") == "yes", accessMinzoom) + AttributeNumeric("layer", tonumber(Find("layer")) or 0, accessMinzoom) + AttributeBoolean("expressway", Find("expressway"), 7) + Attribute("mtb_scale", Find("mtb:scale"), 10) end -- Process way tags -function way_function(way) - local route = way:Find("route") - local highway = way:Find("highway") - local waterway = way:Find("waterway") - local water = way:Find("water") - local building = way:Find("building") - local natural = way:Find("natural") - local historic = way:Find("historic") - local landuse = way:Find("landuse") - local leisure = way:Find("leisure") - local amenity = way:Find("amenity") - local aeroway = way:Find("aeroway") - local railway = way:Find("railway") - local service = way:Find("service") - local sport = way:Find("sport") - local shop = way:Find("shop") - local tourism = way:Find("tourism") - local man_made = way:Find("man_made") - local boundary = way:Find("boundary") - local isClosed = way:IsClosed() - local housenumber = way:Find("addr:housenumber") +function way_function() + local route = Find("route") + local highway = Find("highway") + local waterway = Find("waterway") + local water = Find("water") + local building = Find("building") + local natural = Find("natural") + local historic = Find("historic") + local landuse = Find("landuse") + local leisure = Find("leisure") + local amenity = Find("amenity") + local aeroway = Find("aeroway") + local railway = Find("railway") + local service = Find("service") + local sport = Find("sport") + local shop = Find("shop") + local tourism = Find("tourism") + local man_made = Find("man_made") + local boundary = Find("boundary") + local isClosed = IsClosed() + local housenumber = Find("addr:housenumber") local write_name = false - local construction = way:Find("construction") + local construction = Find("construction") -- Miscellaneous preprocessing - if way:Find("disused") == "yes" then return end - if boundary~="" and way:Find("protection_title")=="National Forest" and way:Find("operator")=="United States Forest Service" then return end + if Find("disused") == "yes" then return end + if boundary~="" and Find("protection_title")=="National Forest" and Find("operator")=="United States Forest Service" then return end if highway == "proposed" then return end if aerowayBuildings[aeroway] then building="yes"; aeroway="" end if landuse == "field" then landuse = "farmland" end - if landuse == "meadow" and way:Find("meadow")=="agricultural" then landuse="farmland" end + if landuse == "meadow" and Find("meadow")=="agricultural" then landuse="farmland" end -- Boundaries within relations -- note that we process administrative boundaries as properties on ways, rather than as single relation geometries, @@ -361,21 +361,21 @@ function way_function(way) local admin_level = 11 local isBoundary = false while true do - local rel = way:NextRelation() + local rel = NextRelation() if not rel then break end isBoundary = true - admin_level = math.min(admin_level, tonumber(way:FindInRelation("admin_level")) or 11) + admin_level = math.min(admin_level, tonumber(FindInRelation("admin_level")) or 11) end -- Boundaries in ways if boundary=="administrative" then - admin_level = math.min(admin_level, tonumber(way:Find("admin_level")) or 11) + admin_level = math.min(admin_level, tonumber(Find("admin_level")) or 11) isBoundary = true end -- Administrative boundaries -- https://openmaptiles.org/schema/#boundary - if isBoundary and not (way:Find("maritime")=="yes") then + if isBoundary and not (Find("maritime")=="yes") then local mz = 0 if admin_level>=3 and admin_level<5 then mz=4 elseif admin_level>=5 and admin_level<7 then mz=8 @@ -383,22 +383,22 @@ function way_function(way) elseif admin_level>=8 then mz=12 end - way:Layer("boundary",false) - way:AttributeNumeric("admin_level", admin_level) - way:MinZoom(mz) + Layer("boundary",false) + AttributeNumeric("admin_level", admin_level) + MinZoom(mz) -- disputed status (0 or 1). some styles need to have the 0 to show it. - local disputed = way:Find("disputed") + local disputed = Find("disputed") if disputed=="yes" then - way:AttributeNumeric("disputed", 1) + AttributeNumeric("disputed", 1) else - way:AttributeNumeric("disputed", 0) + AttributeNumeric("disputed", 0) end end -- Roads ('transportation' and 'transportation_name', plus 'transportation_name_detail') if highway~="" then - local access = way:Find("access") - local surface = way:Find("surface") + local access = Find("access") + local surface = Find("surface") local h = highway local minzoom = 99 @@ -439,159 +439,159 @@ function way_function(way) -- Write to layer if minzoom <= 14 then - write_to_transportation_layer(way, minzoom, h) + write_to_transportation_layer(minzoom, h) -- Write names if minzoom < 8 then minzoom = 8 end if highway == "motorway" or highway == "trunk" then - way:Layer("transportation_name", false) - way:MinZoom(minzoom) + Layer("transportation_name", false) + MinZoom(minzoom) elseif h == "minor" or h == "track" or h == "path" or h == "service" then - way:Layer("transportation_name_detail", false) - way:MinZoom(minzoom) + Layer("transportation_name_detail", false) + MinZoom(minzoom) else - way:Layer("transportation_name_mid", false) - way:MinZoom(minzoom) + Layer("transportation_name_mid", false) + MinZoom(minzoom) end - SetNameAttributes(way) - way:Attribute("class",h) - way:Attribute("network","road") -- **** could also be us-interstate, us-highway, us-state - if h~=highway then way:Attribute("subclass",highway) end - local ref = way:Find("ref") + SetNameAttributes() + Attribute("class",h) + Attribute("network","road") -- **** could also be us-interstate, us-highway, us-state + if h~=highway then Attribute("subclass",highway) end + local ref = Find("ref") if ref~="" then - way:Attribute("ref",ref) - way:AttributeNumeric("ref_length",ref:len()) + Attribute("ref",ref) + AttributeNumeric("ref_length",ref:len()) end end end -- Railways ('transportation' and 'transportation_name', plus 'transportation_name_detail') if railway~="" then - way:Layer("transportation", false) - way:Attribute("class", railway) - SetZOrder(way) - SetBrunnelAttributes(way) + Layer("transportation", false) + Attribute("class", railway) + SetZOrder() + SetBrunnelAttributes() if service~="" then - way:Attribute("service", service) - way:MinZoom(12) + Attribute("service", service) + MinZoom(12) else - way:MinZoom(9) + MinZoom(9) end - way:Layer("transportation_name", false) - SetNameAttributes(way) - way:MinZoom(14) - way:Attribute("class", "rail") + Layer("transportation_name", false) + SetNameAttributes() + MinZoom(14) + Attribute("class", "rail") end -- Pier if man_made=="pier" then - way:Layer("transportation", isClosed) - SetZOrder(way) - way:Attribute("class", "pier") - SetMinZoomByArea(way) + Layer("transportation", isClosed) + SetZOrder() + Attribute("class", "pier") + SetMinZoomByArea() end -- 'Ferry' if route=="ferry" then - way:Layer("transportation", false) - way:Attribute("class", "ferry") - SetZOrder(way) - way:MinZoom(9) - SetBrunnelAttributes(way) + Layer("transportation", false) + Attribute("class", "ferry") + SetZOrder() + MinZoom(9) + SetBrunnelAttributes() - way:Layer("transportation_name", false) - SetNameAttributes(way) - way:MinZoom(12) - way:Attribute("class", "ferry") + Layer("transportation_name", false) + SetNameAttributes() + MinZoom(12) + Attribute("class", "ferry") end -- 'Aeroway' if aeroway~="" then - way:Layer("aeroway", isClosed) - way:Attribute("class",aeroway) - way:Attribute("ref",way:Find("ref")) + Layer("aeroway", isClosed) + Attribute("class",aeroway) + Attribute("ref",Find("ref")) write_name = true end -- 'aerodrome_label' if aeroway=="aerodrome" then - way:LayerAsCentroid("aerodrome_label") - SetNameAttributes(way) - way:Attribute("iata", way:Find("iata")) - SetEleAttributes(way) - way:Attribute("icao", way:Find("icao")) + LayerAsCentroid("aerodrome_label") + SetNameAttributes() + Attribute("iata", Find("iata")) + SetEleAttributes() + Attribute("icao", Find("icao")) - local aerodrome = way:Find(aeroway) + local aerodrome = Find(aeroway) local class if aerodromeValues[aerodrome] then class = aerodrome else class = "other" end - way:Attribute("class", class) + Attribute("class", class) end -- Set 'waterway' and associated if waterwayClasses[waterway] and not isClosed then - if waterway == "river" and way:Holds("name") then - way:Layer("waterway", false) + if waterway == "river" and Holds("name") then + Layer("waterway", false) else - way:Layer("waterway_detail", false) + Layer("waterway_detail", false) end - if way:Find("intermittent")=="yes" then way:AttributeNumeric("intermittent", 1) else way:AttributeNumeric("intermittent", 0) end - way:Attribute("class", waterway) - SetNameAttributes(way) - SetBrunnelAttributes(way) - elseif waterway == "boatyard" then way:Layer("landuse", isClosed); way:Attribute("class", "industrial"); way:MinZoom(12) - elseif waterway == "dam" then way:Layer("building",isClosed) - elseif waterway == "fuel" then way:Layer("landuse", isClosed); way:Attribute("class", "industrial"); way:MinZoom(14) + if Find("intermittent")=="yes" then AttributeNumeric("intermittent", 1) else AttributeNumeric("intermittent", 0) end + Attribute("class", waterway) + SetNameAttributes() + SetBrunnelAttributes() + elseif waterway == "boatyard" then Layer("landuse", isClosed); Attribute("class", "industrial"); MinZoom(12) + elseif waterway == "dam" then Layer("building",isClosed) + elseif waterway == "fuel" then Layer("landuse", isClosed); Attribute("class", "industrial"); MinZoom(14) end -- Set names on rivers if waterwayClasses[waterway] and not isClosed then - if waterway == "river" and way:Holds("name") then - way:Layer("water_name", false) + if waterway == "river" and Holds("name") then + Layer("water_name", false) else - way:Layer("water_name_detail", false) - way:MinZoom(14) + Layer("water_name_detail", false) + MinZoom(14) end - way:Attribute("class", waterway) - SetNameAttributes(way) + Attribute("class", waterway) + SetNameAttributes() end -- Set 'building' and associated if building~="" then - way:Layer("building", true) - SetBuildingHeightAttributes(way) - SetMinZoomByArea(way) + Layer("building", true) + SetBuildingHeightAttributes() + SetMinZoomByArea() end -- Set 'housenumber' if housenumber~="" then - way:LayerAsCentroid("housenumber", false) - way:Attribute("housenumber", housenumber) + LayerAsCentroid("housenumber", false) + Attribute("housenumber", housenumber) end -- Set 'water' if natural=="water" or natural=="bay" or leisure=="swimming_pool" or landuse=="reservoir" or landuse=="basin" or waterClasses[waterway] then - if way:Find("covered")=="yes" or not isClosed then return end + if Find("covered")=="yes" or not isClosed then return end local class="lake"; if natural=="bay" then class="ocean" elseif waterway~="" then class="river" end - if class=="lake" and way:Find("wikidata")=="Q192770" then return end - if class=="ocean" and isClosed and (way:AreaIntersecting("ocean")/way:Area() > 0.98) then return end - way:Layer("water",true) - SetMinZoomByArea(way) - way:Attribute("class",class) + if class=="lake" and Find("wikidata")=="Q192770" then return end + if class=="ocean" and isClosed and (AreaIntersecting("ocean")/Area() > 0.98) then return end + Layer("water",true) + SetMinZoomByArea() + Attribute("class",class) - if way:Find("intermittent")=="yes" then way:Attribute("intermittent",1) end + if Find("intermittent")=="yes" then Attribute("intermittent",1) end -- we only want to show the names of actual lakes not every man-made basin that probably doesn't even have a name other than "basin" -- examples for which we don't want to show a name: -- https://www.openstreetmap.org/way/25958687 -- https://www.openstreetmap.org/way/27201902 -- https://www.openstreetmap.org/way/25309134 -- https://www.openstreetmap.org/way/24579306 - if way:Holds("name") and natural=="water" and water ~= "basin" and water ~= "wastewater" then - way:LayerAsCentroid("water_name_detail") - SetNameAttributes(way) - SetMinZoomByArea(way) - way:Attribute("class", class) + if Holds("name") and natural=="water" and water ~= "basin" and water ~= "wastewater" then + LayerAsCentroid("water_name_detail") + SetNameAttributes() + SetMinZoomByArea() + Attribute("class", class) end return -- in case we get any landuse processing @@ -602,11 +602,11 @@ function way_function(way) if l=="" then l=natural end if l=="" then l=leisure end if landcoverKeys[l] then - way:Layer("landcover", true) - SetMinZoomByArea(way) - way:Attribute("class", landcoverKeys[l]) - if l=="wetland" then way:Attribute("subclass", way:Find("wetland")) - else way:Attribute("subclass", l) end + Layer("landcover", true) + SetMinZoomByArea() + Attribute("class", landcoverKeys[l]) + if l=="wetland" then Attribute("subclass", Find("wetland")) + else Attribute("subclass", l) end write_name = true -- Set 'landuse' @@ -614,31 +614,31 @@ function way_function(way) if l=="" then l=amenity end if l=="" then l=tourism end if landuseKeys[l] then - way:Layer("landuse", true) - way:Attribute("class", l) + Layer("landuse", true) + Attribute("class", l) if l=="residential" then - if way:Area()4 then layer="poi_detail" end - obj:LayerAsCentroid(layer) - SetNameAttributes(obj) - obj:AttributeNumeric("rank", rank) - obj:Attribute("class", class) - obj:Attribute("subclass", subclass) + LayerAsCentroid(layer) + SetNameAttributes() + AttributeNumeric("rank", rank) + Attribute("class", class) + Attribute("subclass", subclass) -- layer defaults to 0 - obj:AttributeNumeric("layer", tonumber(obj:Find("layer")) or 0) + AttributeNumeric("layer", tonumber(Find("layer")) or 0) -- indoor defaults to false - obj:AttributeBoolean("indoor", (obj:Find("indoor") == "yes")) + AttributeBoolean("indoor", (Find("indoor") == "yes")) -- level has no default - local level = tonumber(obj:Find("level")) + local level = tonumber(Find("level")) if level then - obj:AttributeNumeric("level", level) + AttributeNumeric("level", level) end end -- Set name attributes on any object -function SetNameAttributes(obj) - local name = obj:Find("name"), iname +function SetNameAttributes() + local name = Find("name"), iname local main_written = name -- if we have a preferred language, then write that (if available), and additionally write the base name tag - if preferred_language and obj:Holds("name:"..preferred_language) then - iname = obj:Find("name:"..preferred_language) - obj:Attribute(preferred_language_attribute, iname) + if preferred_language and Holds("name:"..preferred_language) then + iname = Find("name:"..preferred_language) + Attribute(preferred_language_attribute, iname) if iname~=name and default_language_attribute then - obj:Attribute(default_language_attribute, name) + Attribute(default_language_attribute, name) else main_written = iname end else - obj:Attribute(preferred_language_attribute, name) + Attribute(preferred_language_attribute, name) end -- then set any additional languages for i,lang in ipairs(additional_languages) do - iname = obj:Find("name:"..lang) + iname = Find("name:"..lang) if iname=="" then iname=name end - if iname~=main_written then obj:Attribute("name:"..lang, iname) end + if iname~=main_written then Attribute("name:"..lang, iname) end end end -- Set ele and ele_ft on any object -function SetEleAttributes(obj) - local ele = obj:Find("ele") +function SetEleAttributes() + local ele = Find("ele") if ele ~= "" then local meter = math.floor(tonumber(ele) or 0) local feet = math.floor(meter * 3.2808399) - obj:AttributeNumeric("ele", meter) - obj:AttributeNumeric("ele_ft", feet) + AttributeNumeric("ele", meter) + AttributeNumeric("ele_ft", feet) end end -function SetBrunnelAttributes(obj) - if obj:Find("bridge") == "yes" then obj:Attribute("brunnel", "bridge") - elseif obj:Find("tunnel") == "yes" then obj:Attribute("brunnel", "tunnel") - elseif obj:Find("ford") == "yes" then obj:Attribute("brunnel", "ford") +function SetBrunnelAttributes() + if Find("bridge") == "yes" then Attribute("brunnel", "bridge") + elseif Find("tunnel") == "yes" then Attribute("brunnel", "tunnel") + elseif Find("ford") == "yes" then Attribute("brunnel", "ford") end end -- Set minimum zoom level by area -function SetMinZoomByArea(way) - local area=way:Area() - if area>ZRES5^2 then way:MinZoom(6) - elseif area>ZRES6^2 then way:MinZoom(7) - elseif area>ZRES7^2 then way:MinZoom(8) - elseif area>ZRES8^2 then way:MinZoom(9) - elseif area>ZRES9^2 then way:MinZoom(10) - elseif area>ZRES10^2 then way:MinZoom(11) - elseif area>ZRES11^2 then way:MinZoom(12) - elseif area>ZRES12^2 then way:MinZoom(13) - else way:MinZoom(14) end +function SetMinZoomByArea() + local area=Area() + if area>ZRES5^2 then MinZoom(6) + elseif area>ZRES6^2 then MinZoom(7) + elseif area>ZRES7^2 then MinZoom(8) + elseif area>ZRES8^2 then MinZoom(9) + elseif area>ZRES9^2 then MinZoom(10) + elseif area>ZRES10^2 then MinZoom(11) + elseif area>ZRES11^2 then MinZoom(12) + elseif area>ZRES12^2 then MinZoom(13) + else MinZoom(14) end end -- Calculate POIs (typically rank 1-4 go to 'poi' z12-14, rank 5+ to 'poi_detail' z14) -- returns rank, class, subclass -function GetPOIRank(obj) +function GetPOIRank() local k,list,v,class,rank -- Can we find the tag? for k,list in pairs(poiTags) do - if list[obj:Find(k)] then - v = obj:Find(k) -- k/v are the OSM tag pair + if list[Find(k)] then + v = Find(k) -- k/v are the OSM tag pair class = poiClasses[v] or k rank = poiClassRanks[class] or 25 subclassKey = poiSubClasses[v] if subclassKey then class = v - v = obj:Find(subclassKey) + v = Find(subclassKey) end return rank, class, v end end -- Catch-all for shops - local shop = obj:Find("shop") + local shop = Find("shop") if shop~="" then return poiClassRanks['shop'], "shop", shop end -- Nothing found return nil,nil,nil end -function SetBuildingHeightAttributes(way) - local height = tonumber(way:Find("height"), 10) - local minHeight = tonumber(way:Find("min_height"), 10) - local levels = tonumber(way:Find("building:levels"), 10) - local minLevel = tonumber(way:Find("building:min_level"), 10) +function SetBuildingHeightAttributes() + local height = tonumber(Find("height"), 10) + local minHeight = tonumber(Find("min_height"), 10) + local levels = tonumber(Find("building:levels"), 10) + local minLevel = tonumber(Find("building:min_level"), 10) local renderHeight = BUILDING_FLOOR_HEIGHT if height or levels then @@ -780,17 +780,17 @@ function SetBuildingHeightAttributes(way) renderHeight = renderHeight + renderMinHeight end - way:AttributeNumeric("render_height", renderHeight) - way:AttributeNumeric("render_min_height", renderMinHeight) + AttributeNumeric("render_height", renderHeight) + AttributeNumeric("render_min_height", renderMinHeight) end -- Implement z_order as calculated by Imposm -- See https://imposm.org/docs/imposm3/latest/mapping.html#wayzorder for details. -function SetZOrder(way) - local highway = way:Find("highway") - local layer = tonumber(way:Find("layer")) - local bridge = way:Find("bridge") - local tunnel = way:Find("tunnel") +function SetZOrder() + local highway = Find("highway") + local layer = tonumber(Find("layer")) + local bridge = Find("bridge") + local tunnel = Find("tunnel") local zOrder = 0 if bridge ~= "" and bridge ~= "no" then zOrder = zOrder + 10 @@ -821,7 +821,7 @@ function SetZOrder(way) hwClass = 3 end zOrder = zOrder + hwClass - way:ZOrder(zOrder) + ZOrder(zOrder) end -- ========================================================== diff --git a/resources/process_coastline.lua b/resources/process_coastline.lua index 5e2aca8e..b49eeee5 100644 --- a/resources/process_coastline.lua +++ b/resources/process_coastline.lua @@ -10,10 +10,10 @@ function exit_function() end node_keys = {} -function node_function(node) +function node_function() end -function way_function(way) +function way_function() end -- Remap coastlines diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index d8c1dd65..9cbaf9ef 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -7,6 +7,31 @@ using namespace std; thread_local kaguya::State *g_luaState = nullptr; +thread_local OsmLuaProcessing* osmLuaProcessing = nullptr; + +std::string rawId() { return osmLuaProcessing->Id(); } +bool rawHolds(const std::string& key) { return osmLuaProcessing->Holds(key); } +const std::string& rawFind(const std::string& key) { return osmLuaProcessing->Find(key); } +std::vector rawFindIntersecting(const std::string &layerName) { return osmLuaProcessing->FindIntersecting(layerName); } +bool rawIntersects(const std::string& layerName) { return osmLuaProcessing->Intersects(layerName); } +std::vector rawFindCovering(const std::string& layerName) { return osmLuaProcessing->FindCovering(layerName); } +bool rawCoveredBy(const std::string& layerName) { return osmLuaProcessing->CoveredBy(layerName); } +bool rawIsClosed() { return osmLuaProcessing->IsClosed(); } +double rawArea() { return osmLuaProcessing->Area(); } +double rawLength() { return osmLuaProcessing->Length(); } +std::vector Centroid() { return osmLuaProcessing->Centroid(); } +void rawLayer(const std::string& layerName, bool area) { return osmLuaProcessing->Layer(layerName, area); } +void rawLayerAsCentroid(const std::string &layerName) { return osmLuaProcessing->LayerAsCentroid(layerName); } +void rawMinZoom(const double z) { return osmLuaProcessing->MinZoom(z); } +void rawZOrder(const double z) { return osmLuaProcessing->ZOrder(z); } +kaguya::optional rawNextRelation() { return osmLuaProcessing->NextRelation(); } +void rawRestartRelations() { return osmLuaProcessing->RestartRelations(); } +std::string rawFindInRelation(const std::string& key) { return osmLuaProcessing->FindInRelation(key); } +void rawAccept() { return osmLuaProcessing->Accept(); } +double rawAreaIntersecting(const std::string& layerName) { return osmLuaProcessing->AreaIntersecting(layerName); } +std::vector rawCentroid() { return osmLuaProcessing->Centroid(); } + + bool supportsRemappingShapefiles = false; const std::string EMPTY_STRING = ""; @@ -39,31 +64,41 @@ OsmLuaProcessing::OsmLuaProcessing( g_luaState = &luaState; luaState.setErrorHandler(lua_error_handler); luaState.dofile(luaFile.c_str()); - luaState["OSM"].setClass(kaguya::UserdataMetatable() - .addFunction("Id", &OsmLuaProcessing::Id) - .addFunction("Holds", &OsmLuaProcessing::Holds) - .addFunction("Find", &OsmLuaProcessing::Find) - .addFunction("FindIntersecting", &OsmLuaProcessing::FindIntersecting) - .addFunction("Intersects", &OsmLuaProcessing::Intersects) - .addFunction("FindCovering", &OsmLuaProcessing::FindCovering) - .addFunction("CoveredBy", &OsmLuaProcessing::CoveredBy) - .addFunction("IsClosed", &OsmLuaProcessing::IsClosed) - .addFunction("Area", &OsmLuaProcessing::Area) - .addFunction("AreaIntersecting", &OsmLuaProcessing::AreaIntersecting) - .addFunction("Length", &OsmLuaProcessing::Length) - .addFunction("Centroid", &OsmLuaProcessing::Centroid) - .addFunction("Layer", &OsmLuaProcessing::Layer) - .addFunction("LayerAsCentroid", &OsmLuaProcessing::LayerAsCentroid) - .addOverloadedFunctions("Attribute", &OsmLuaProcessing::Attribute, &OsmLuaProcessing::AttributeWithMinZoom) - .addOverloadedFunctions("AttributeNumeric", &OsmLuaProcessing::AttributeNumeric, &OsmLuaProcessing::AttributeNumericWithMinZoom) - .addOverloadedFunctions("AttributeBoolean", &OsmLuaProcessing::AttributeBoolean, &OsmLuaProcessing::AttributeBooleanWithMinZoom) - .addFunction("MinZoom", &OsmLuaProcessing::MinZoom) - .addFunction("ZOrder", &OsmLuaProcessing::ZOrder) - .addFunction("Accept", &OsmLuaProcessing::Accept) - .addFunction("NextRelation", &OsmLuaProcessing::NextRelation) - .addFunction("RestartRelations", &OsmLuaProcessing::RestartRelations) - .addFunction("FindInRelation", &OsmLuaProcessing::FindInRelation) + + osmLuaProcessing = this; + luaState["Id"] = &rawId; + luaState["Holds"] = &rawHolds; + luaState["Find"] = &rawFind; + luaState["FindIntersecting"] = &rawFindIntersecting; + luaState["Intersects"] = &rawIntersects; + luaState["FindCovering"] = &rawFindCovering; + luaState["CoveredBy"] = &rawCoveredBy; + luaState["IsClosed"] = &rawIsClosed; + luaState["Area"] = &rawArea; + luaState["AreaIntersecting"] = &rawAreaIntersecting; + luaState["Length"] = &rawLength; + luaState["Centroid"] = &rawCentroid; + luaState["Layer"] = &rawLayer; + luaState["LayerAsCentroid"] = &rawLayerAsCentroid; + luaState["Attribute"] = kaguya::overload( + [](const std::string &key, const std::string& val) { osmLuaProcessing->Attribute(key, val); }, + [](const std::string &key, const std::string& val, const char minzoom) { osmLuaProcessing->AttributeWithMinZoom(key, val, minzoom); } ); + luaState["AttributeNumeric"] = kaguya::overload( + [](const std::string &key, const float val) { osmLuaProcessing->AttributeNumeric(key, val); }, + [](const std::string &key, const float val, const char minzoom) { osmLuaProcessing->AttributeNumericWithMinZoom(key, val, minzoom); } + ); + luaState["AttributeBoolean"] = kaguya::overload( + [](const std::string &key, const bool val) { osmLuaProcessing->AttributeBoolean(key, val); }, + [](const std::string &key, const bool val, const char minzoom) { osmLuaProcessing->AttributeBooleanWithMinZoom(key, val, minzoom); } + ); + + luaState["MinZoom"] = &rawMinZoom; + luaState["ZOrder"] = &rawZOrder; + luaState["Accept"] = &rawAccept; + luaState["NextRelation"] = &rawNextRelation; + luaState["RestartRelations"] = &rawRestartRelations; + luaState["FindInRelation"] = &rawFindInRelation; supportsRemappingShapefiles = !!luaState["attribute_function"]; supportsReadingRelations = !!luaState["relation_scan_function"]; supportsWritingRelations = !!luaState["relation_function"]; @@ -534,7 +569,7 @@ bool OsmLuaProcessing::scanRelation(WayID id, const tag_map_t &tags) { isRelation = true; currentTags = &tags; try { - luaState["relation_scan_function"](this); + luaState["relation_scan_function"](); } catch(luaProcessingException &e) { std::cerr << "Lua error on scanning relation " << originalOsmID << std::endl; exit(1); @@ -557,7 +592,7 @@ void OsmLuaProcessing::setNode(NodeID id, LatpLon node, const tag_map_t &tags) { //Start Lua processing for node try { - luaState["node_function"](this); + luaState["node_function"](); } catch(luaProcessingException &e) { std::cerr << "Lua error on node " << originalOsmID << std::endl; exit(1); @@ -605,7 +640,7 @@ void OsmLuaProcessing::setWay(WayID wayId, LatpLonVec const &llVec, const tag_ma //Start Lua processing for way try { kaguya::LuaFunction way_function = luaState["way_function"]; - kaguya::LuaRef ret = way_function(this); + kaguya::LuaRef ret = way_function(); assert(!ret); } catch(luaProcessingException &e) { std::cerr << "Lua error on way " << originalOsmID << std::endl; @@ -636,7 +671,7 @@ void OsmLuaProcessing::setRelation(int64_t relationId, WayVec const &outerWayVec // Start Lua processing for relation if (!isNativeMP && !supportsWritingRelations) return; try { - luaState[isNativeMP ? "way_function" : "relation_function"](this); + luaState[isNativeMP ? "way_function" : "relation_function"](); } catch(luaProcessingException &e) { std::cerr << "Lua error on relation " << originalOsmID << std::endl; exit(1); From b3221667a9d2366410dbfdc7f25f3062d7a135ef Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 3 Dec 2023 17:16:32 -0500 Subject: [PATCH 02/81] spike: use different tag map Building a std::map for tags is somewhat expensive, especially when we know that the number of tags is usually quite small. Instead, use a custom structure that does a crappy-but-fast hash to put the keys/values in one of 16 buckets, then linear search the bucket. For GB, before: ``` real 1m11.507s user 16m49.604s sys 0m17.381s ``` After: ``` real 1m9.557s user 16m28.826s sys 0m17.937s ``` Saving 2 seconds of wall clock and 20 seconds of user time doesn't seem like much, but (a) it's not nothing and (b) having the tags in this format will enable us to thwart some of Lua's defensive copies in a subsequent commit. A note about the hash function: hashing each letter of the string using boost::hash_combine eliminated the time savings. --- Makefile | 4 +- include/osm_lua_processing.h | 13 ++++-- include/read_pbf.h | 11 +++++ include/tag_map.h | 43 +++++++++++++++++ src/osm_lua_processing.cpp | 21 +++++---- src/read_pbf.cpp | 18 ++++--- src/tag_map.cpp | 91 ++++++++++++++++++++++++++++++++++++ src/tilemaker.cpp | 1 + 8 files changed, 180 insertions(+), 22 deletions(-) create mode 100644 include/tag_map.h create mode 100644 src/tag_map.cpp diff --git a/Makefile b/Makefile index 6a510b38..223b1be7 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,7 @@ prefix = /usr/local MANPREFIX := /usr/share/man TM_VERSION ?= $(shell git describe --tags --abbrev=0) -CXXFLAGS ?= -O3 -Wall -Wno-unknown-pragmas -Wno-sign-compare -std=c++14 -pthread -fPIE -DTM_VERSION=$(TM_VERSION) $(CONFIG) +CXXFLAGS ?= -O3 -Wall -Wno-unknown-pragmas -Wno-sign-compare -std=c++14 -pthread -fPIE -DTM_VERSION=$(TM_VERSION) $(CONFIG) -g LIB := -L$(PLATFORM_PATH)/lib -lz $(LUA_LIBS) -lboost_program_options -lsqlite3 -lboost_filesystem -lboost_system -lboost_iostreams -lprotobuf -lshp -pthread INC := -I$(PLATFORM_PATH)/include -isystem ./include -I./src $(LUA_CFLAGS) @@ -91,7 +91,7 @@ INC := -I$(PLATFORM_PATH)/include -isystem ./include -I./src $(LUA_CFLAGS) all: tilemaker -tilemaker: include/osmformat.pb.o include/vector_tile.pb.o src/mbtiles.o src/pbf_blocks.o src/coordinates.o src/osm_store.o src/helpers.o src/output_object.o src/read_shp.o src/read_pbf.o src/osm_lua_processing.o src/write_geometry.o src/shared_data.o src/tile_worker.o src/tile_data.o src/osm_mem_tiles.o src/shp_mem_tiles.o src/attribute_store.o src/tilemaker.o src/geom.o +tilemaker: include/osmformat.pb.o include/vector_tile.pb.o src/mbtiles.o src/pbf_blocks.o src/coordinates.o src/osm_store.o src/helpers.o src/output_object.o src/read_shp.o src/read_pbf.o src/osm_lua_processing.o src/write_geometry.o src/shared_data.o src/tile_worker.o src/tile_data.o src/osm_mem_tiles.o src/shp_mem_tiles.o src/attribute_store.o src/tilemaker.o src/geom.o src/tag_map.o $(CXX) $(CXXFLAGS) -o tilemaker $^ $(INC) $(LIB) $(LDFLAGS) %.o: %.cpp diff --git a/include/osm_lua_processing.h b/include/osm_lua_processing.h index 6fd28cb0..ee231483 100644 --- a/include/osm_lua_processing.h +++ b/include/osm_lua_processing.h @@ -16,6 +16,8 @@ #include +class TagMap; + // Lua extern "C" { #include "lua.h" @@ -71,19 +73,19 @@ class OsmLuaProcessing { using tag_map_t = boost::container::flat_map; // Scan non-MP relation - bool scanRelation(WayID id, const tag_map_t &tags); + bool scanRelation(WayID id, const TagMap& tags); /// \brief We are now processing a significant node - void setNode(NodeID id, LatpLon node, const tag_map_t &tags); + void setNode(NodeID id, LatpLon node, const TagMap& tags); /// \brief We are now processing a way - void setWay(WayID wayId, LatpLonVec const &llVec, const tag_map_t &tags); + void setWay(WayID wayId, LatpLonVec const &llVec, const TagMap& tags); /** \brief We are now processing a relation * (note that we store relations as ways with artificial IDs, and that * we use decrementing positive IDs to give a bit more space for way IDs) */ - void setRelation(int64_t relationId, WayVec const &outerWayVec, WayVec const &innerWayVec, const tag_map_t &tags, bool isNativeMP, bool isInnerOuter); + void setRelation(int64_t relationId, WayVec const &outerWayVec, WayVec const &innerWayVec, const TagMap& tags, bool isNativeMP, bool isInnerOuter); // ---- Metadata queries called from Lua @@ -253,7 +255,8 @@ class OsmLuaProcessing { class LayerDefinition &layers; std::vector> outputs; // All output objects that have been created - const boost::container::flat_map* currentTags; + //const boost::container::flat_map* currentTags; + const TagMap* currentTags; std::vector finalizeOutputs(); }; diff --git a/include/read_pbf.h b/include/read_pbf.h index 7761dc58..1df6b440 100644 --- a/include/read_pbf.h +++ b/include/read_pbf.h @@ -11,6 +11,7 @@ // Protobuf #include "osmformat.pb.h" #include "vector_tile.pb.h" +#include "tag_map.h" class OsmLuaProcessing; @@ -34,6 +35,7 @@ class PbfReader pbfreader_generate_output const &generate_output); // Read tags into a map from a way/node/relation + /* using tag_map_t = boost::container::flat_map; template void readTags(T &pbfObject, PrimitiveBlock const &pb, tag_map_t &tags) { @@ -44,6 +46,15 @@ class PbfReader tags[pb.stringtable().s(keysPtr->Get(n))] = pb.stringtable().s(valsPtr->Get(n)); } } + */ + template + void readTags(T &pbfObject, PrimitiveBlock const &pb, TagMap& tags) { + auto keysPtr = pbfObject.mutable_keys(); + auto valsPtr = pbfObject.mutable_vals(); + for (uint n=0; n < pbfObject.keys_size(); n++) { + tags.addTag(pb.stringtable().s(keysPtr->Get(n)), pb.stringtable().s(valsPtr->Get(n))); + } + } private: bool ReadBlock(std::istream &infile, OsmLuaProcessing &output, std::pair progress, std::size_t datasize, diff --git a/include/tag_map.h b/include/tag_map.h new file mode 100644 index 00000000..9bfa799a --- /dev/null +++ b/include/tag_map.h @@ -0,0 +1,43 @@ +#ifndef _TAG_MAP_H +#define _TAG_MAP_H + +#include +#include +#include + +// We track tags in a special structure, which enables some tricks when +// doing Lua interop. +// +// The alternative is a std::map - but often, our map is quite small. +// It's preferable to have a small set of vectors and do linear search. +// +// Further, we can avoid passing std::string from Lua -> C++ in some cases +// by first checking to see if the string we would have passed is already +// stored in our tag map, and passing a reference to its location. + +// Assumptions: +// 1. Not thread-safe. +// 2. Lifetime of map is less than lifetime of keys/values that are passed. +class TagMap { +public: + TagMap(); + void reset(); + + void addTag(const std::string& key, const std::string& value); + const std::string* getTag(const std::string& key) const; + + boost::container::flat_map exportToBoostMap() const; + +private: + uint32_t ensureString( + std::vector>& vector, + const std::string& value + ); + + + std::vector> keys; + std::vector> key2value; + std::vector> values; +}; + +#endif _TAG_MAP_H diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index 9cbaf9ef..0f76b6d3 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -2,6 +2,7 @@ #include "attribute_store.h" #include "helpers.h" #include +#include "tag_map.h" using namespace std; @@ -152,14 +153,14 @@ string OsmLuaProcessing::Id() const { // Check if there's a value for a given key bool OsmLuaProcessing::Holds(const string& key) const { - return currentTags->find(key) != currentTags->end(); + return currentTags->getTag(key) != nullptr; } // Get an OSM tag for a given key (or return empty string if none) const string& OsmLuaProcessing::Find(const string& key) const { - auto it = currentTags->find(key); - if(it == currentTags->end()) return EMPTY_STRING; - return it->second; + auto it = currentTags->getTag(key); + if(it == nullptr) return EMPTY_STRING; + return *it; } // ---- Spatial queries called from Lua @@ -562,7 +563,7 @@ void OsmLuaProcessing::setVectorLayerMetadata(const uint_least8_t layer, const s // Scan relation (but don't write geometry) // return true if we want it, false if we don't -bool OsmLuaProcessing::scanRelation(WayID id, const tag_map_t &tags) { +bool OsmLuaProcessing::scanRelation(WayID id, const TagMap& tags) { reset(); originalOsmID = id; isWay = false; @@ -576,11 +577,13 @@ bool OsmLuaProcessing::scanRelation(WayID id, const tag_map_t &tags) { } if (!relationAccepted) return false; - osmStore.store_relation_tags(id, tags); + // If we're persisting, we need to make a real map that owns its + // own keys and values. + osmStore.store_relation_tags(id, tags.exportToBoostMap()); return true; } -void OsmLuaProcessing::setNode(NodeID id, LatpLon node, const tag_map_t &tags) { +void OsmLuaProcessing::setNode(NodeID id, LatpLon node, const TagMap& tags) { reset(); originalOsmID = id; @@ -608,7 +611,7 @@ void OsmLuaProcessing::setNode(NodeID id, LatpLon node, const tag_map_t &tags) { } // We are now processing a way -void OsmLuaProcessing::setWay(WayID wayId, LatpLonVec const &llVec, const tag_map_t &tags) { +void OsmLuaProcessing::setWay(WayID wayId, LatpLonVec const &llVec, const TagMap& tags) { reset(); originalOsmID = wayId; isWay = true; @@ -654,7 +657,7 @@ void OsmLuaProcessing::setWay(WayID wayId, LatpLonVec const &llVec, const tag_ma } // We are now processing a relation -void OsmLuaProcessing::setRelation(int64_t relationId, WayVec const &outerWayVec, WayVec const &innerWayVec, const tag_map_t &tags, +void OsmLuaProcessing::setRelation(int64_t relationId, WayVec const &outerWayVec, WayVec const &innerWayVec, const TagMap& tags, bool isNativeMP, // only OSM type=multipolygon bool isInnerOuter) { // any OSM relation with "inner" and "outer" roles (e.g. type=multipolygon|boundary) reset(); diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 01b45a9f..1d795e9e 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -1,6 +1,7 @@ #include #include "read_pbf.h" #include "pbf_blocks.h" +#include "tag_map.h" #include #include @@ -18,6 +19,7 @@ PbfReader::PbfReader(OSMStore &osmStore) bool PbfReader::ReadNodes(OsmLuaProcessing &output, PrimitiveGroup &pg, PrimitiveBlock const &pb, const unordered_set &nodeKeyPositions) { // ---- Read nodes + TagMap tags; if (pg.has_dense()) { int64_t nodeId = 0; @@ -49,11 +51,11 @@ bool PbfReader::ReadNodes(OsmLuaProcessing &output, PrimitiveGroup &pg, Primitiv if (significant) { // For tagged nodes, call Lua, then save the OutputObject - boost::container::flat_map tags; - tags.reserve(kvPos / 2); + tags.reset(); for (uint n=kvStart; n(nodeId), node, tags); } @@ -70,6 +72,7 @@ bool PbfReader::ReadWays(OsmLuaProcessing &output, PrimitiveGroup &pg, Primitive // ---- Read ways if (pg.ways_size() > 0) { + TagMap tags; Way pbfWay; std::vector ways; @@ -103,7 +106,7 @@ bool PbfReader::ReadWays(OsmLuaProcessing &output, PrimitiveGroup &pg, Primitive if (llVec.empty()) continue; try { - tag_map_t tags; + tags.reset(); readTags(pbfWay, pb, tags); // If we need it for later, store the way's coordinates in the global way store @@ -132,6 +135,8 @@ bool PbfReader::ScanRelations(OsmLuaProcessing &output, PrimitiveGroup &pg, Prim int typeKey = findStringPosition(pb, "type"); int mpKey = findStringPosition(pb, "multipolygon"); + TagMap tags; + for (int j=0; j(pbfRelation.id()); if (!isMultiPolygon) { if (output.canReadRelations()) { - tag_map_t tags; + tags.reset(); readTags(pbfRelation, pb, tags); isAccepted = output.scanRelation(relid, tags); } @@ -161,6 +166,7 @@ bool PbfReader::ReadRelations(OsmLuaProcessing &output, PrimitiveGroup &pg, Prim // ---- Read relations if (pg.relations_size() > 0) { + TagMap tags; std::vector relations; int typeKey = findStringPosition(pb, "type"); @@ -189,7 +195,7 @@ bool PbfReader::ReadRelations(OsmLuaProcessing &output, PrimitiveGroup &pg, Prim } try { - tag_map_t tags; + tags.reset(); readTags(pbfRelation, pb, tags); output.setRelation(pbfRelation.id(), outerWayVec, innerWayVec, tags, isMultiPolygon, isInnerOuter); diff --git a/src/tag_map.cpp b/src/tag_map.cpp new file mode 100644 index 00000000..00962e8e --- /dev/null +++ b/src/tag_map.cpp @@ -0,0 +1,91 @@ +#include "tag_map.h" +#include +#include + +TagMap::TagMap() { + keys.resize(16); + key2value.resize(16); + values.resize(16); +} + +void TagMap::reset() { + for (int i = 0; i < 16; i++) { + keys[i].clear(); + key2value[i].clear(); + values[i].clear(); + } +} + +const std::size_t hashString(const std::string& str) { + // This is a pretty crappy hash function in terms of bit + // avalanching and distribution of output values. + // + // But it's very good in terms of speed, which turns out + // to be the important measure. + std::size_t hash = str.size(); + if (hash >= 4) + hash ^= *(uint32_t*)str.data(); + + return hash; +} + +uint32_t TagMap::ensureString( + std::vector>& vector, + const std::string& value +) { + std::size_t hash = hashString(value); + + const uint16_t shard = hash % vector.size(); + for (int i = 0; i < vector[shard].size(); i++) + if (*(vector[shard][i]) == value) + return shard << 16 | i; + + vector[shard].push_back(&value); + return shard << 16 | (vector[shard].size() - 1); +} + + +void TagMap::addTag(const std::string& key, const std::string& value) { + uint32_t valueLoc = ensureString(values, value); +// std::cout << "valueLoc = " << valueLoc << std::endl; + uint32_t keyLoc = ensureString(keys, key); +// std::cout << "keyLoc = " << keyLoc << std::endl; + + + const uint16_t shard = keyLoc >> 16; + const uint16_t pos = keyLoc; +// std::cout << "shard=" << shard << ", pos=" << pos << std::endl; + if (key2value[shard].size() <= pos) { +// std::cout << "growing shard" << std::endl; + key2value[shard].resize(pos + 1); + } + + key2value[shard][pos] = valueLoc; +} + +const std::string* TagMap::getTag(const std::string& key) const { + // Returns nullptr if absent, else pointer to value. + std::size_t hash = hashString(key); + + const uint16_t shard = hash % keys.size(); + for (int i = 0; i < keys[shard].size(); i++) + if (*(keys[shard][i]) == key) { + const uint32_t valueLoc = key2value[shard][i]; + return values[valueLoc >> 16][valueLoc & 0xFFFF]; + } + + return nullptr; +} + +boost::container::flat_map TagMap::exportToBoostMap() const { + boost::container::flat_map rv; + + for (int i = 0; i < keys.size(); i++) { + for (int j = 0; j < keys[i].size(); j++) { + uint32_t valueLoc = key2value[i][j]; + rv[*keys[i][j]] = *values[valueLoc >> 16][valueLoc & 0xFFFF]; + } + } + + return rv; +} diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index 9643f226..5c2e19df 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -392,6 +392,7 @@ int main(int argc, char* argv[]) { } // ---- Write out data + return 0; // TODO // If mapsplit, read list of tiles available unsigned runs=1; From 5c807a9841b866c6dc403141effd4c9d14459034 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 3 Dec 2023 18:29:12 -0500 Subject: [PATCH 03/81] make Find(...) and Holds(...) faster We (ab?)use kaguya's parameter serialization machinery. Rather than take a `std::string`, we take a `KnownTagKey` and teach Lua how to convert a Lua string into a `KnownTagKey`. This avoids the need to do a defensive copy of the string when coming from Lua. It provides a modest boost: ``` real 1m8.859s user 16m13.292s sys 0m18.104s ``` Most keys are short enough to fit in the small-string optimization, so this doesn't help us avoid mallocs. An exception is `addr:housenumber`, which, at 16 bytes, exceeds g++'s limit of 15 bytes. It should be possible to also apply a similar trick to the `Attribute(...)` functions, to avoid defensive copies of strings that we've seen as keys or values. --- Makefile | 2 +- include/osm_lua_processing.h | 9 +---- include/tag_map.h | 13 ++++++-- src/osm_lua_processing.cpp | 64 +++++++++++++++++++++++++++--------- src/tag_map.cpp | 35 ++++++++++++++++++++ src/tilemaker.cpp | 2 +- 6 files changed, 98 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index 223b1be7..29218eff 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,7 @@ prefix = /usr/local MANPREFIX := /usr/share/man TM_VERSION ?= $(shell git describe --tags --abbrev=0) -CXXFLAGS ?= -O3 -Wall -Wno-unknown-pragmas -Wno-sign-compare -std=c++14 -pthread -fPIE -DTM_VERSION=$(TM_VERSION) $(CONFIG) -g +CXXFLAGS ?= -O3 -Wall -Wno-unknown-pragmas -Wno-sign-compare -std=c++14 -pthread -fPIE -DTM_VERSION=$(TM_VERSION) $(CONFIG) LIB := -L$(PLATFORM_PATH)/lib -lz $(LUA_LIBS) -lboost_program_options -lsqlite3 -lboost_filesystem -lboost_system -lboost_iostreams -lprotobuf -lshp -pthread INC := -I$(PLATFORM_PATH)/include -isystem ./include -I./src $(LUA_CFLAGS) diff --git a/include/osm_lua_processing.h b/include/osm_lua_processing.h index ee231483..c74e8800 100644 --- a/include/osm_lua_processing.h +++ b/include/osm_lua_processing.h @@ -92,12 +92,6 @@ class OsmLuaProcessing { // Get the ID of the current object std::string Id() const; - // Check if there's a value for a given key - bool Holds(const std::string& key) const; - - // Get an OSM tag for a given key (or return empty string if none) - const std::string& Find(const std::string& key) const; - // ---- Spatial queries called from Lua // Find intersecting shapefile layer @@ -196,6 +190,7 @@ class OsmLuaProcessing { inline AttributeStore &getAttributeStore() { return attributeStore; } struct luaProcessingException :std::exception {}; + const TagMap* currentTags; private: /// Internal: clear current cached state @@ -255,8 +250,6 @@ class OsmLuaProcessing { class LayerDefinition &layers; std::vector> outputs; // All output objects that have been created - //const boost::container::flat_map* currentTags; - const TagMap* currentTags; std::vector finalizeOutputs(); }; diff --git a/include/tag_map.h b/include/tag_map.h index 9bfa799a..af45e76d 100644 --- a/include/tag_map.h +++ b/include/tag_map.h @@ -16,8 +16,12 @@ // stored in our tag map, and passing a reference to its location. // Assumptions: -// 1. Not thread-safe. -// 2. Lifetime of map is less than lifetime of keys/values that are passed. +// 1. Not thread-safe +// This is OK because we have 1 instance of OsmLuaProcessing per thread. +// 2. Lifetime of map is less than lifetime of keys/values that are passed +// This is true since the strings are owned by the protobuf block reader +// 3. Max number of tag values will fit in a short +// OSM limit is 5,000 tags per object class TagMap { public: TagMap(); @@ -26,6 +30,11 @@ class TagMap { void addTag(const std::string& key, const std::string& value); const std::string* getTag(const std::string& key) const; + // Return -1 if key not found, else return its keyLoc. + int64_t getTag(const char* key, size_t size) const; + + const std::string* getValue(uint32_t keyLoc) const; + boost::container::flat_map exportToBoostMap() const; private: diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index 0f76b6d3..cb5cb889 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -7,12 +7,59 @@ using namespace std; +const std::string EMPTY_STRING = ""; thread_local kaguya::State *g_luaState = nullptr; thread_local OsmLuaProcessing* osmLuaProcessing = nullptr; +// A key in `currentTags`. If Lua code refers to an absent key, +// found will be false. +struct KnownTagKey { + bool found; + uint32_t index; +}; + +template<> struct kaguya::lua_type_traits { + typedef KnownTagKey get_type; + typedef const KnownTagKey& push_type; + + static bool strictCheckType(lua_State* l, int index) + { + return lua_type(l, index) == LUA_TSTRING; + } + static bool checkType(lua_State* l, int index) + { + return lua_isstring(l, index) != 0; + } + static get_type get(lua_State* l, int index) + { + KnownTagKey rv = { false, 0 }; + size_t size = 0; + const char* buffer = lua_tolstring(l, index, &size); + + int64_t tagLoc = osmLuaProcessing->currentTags->getTag(buffer, size); + + if (tagLoc >= 0) { + rv.found = true; + rv.index = tagLoc; + } +// std::string key(buffer, size); +// std::cout << "for key " << key << ": rv.found=" << rv.found << ", rv.index=" << rv.index << std::endl; + return rv; + } + static int push(lua_State* l, push_type s) + { + throw std::runtime_error("Lua code doesn't know how to use KnownTagKey"); + } +}; + std::string rawId() { return osmLuaProcessing->Id(); } -bool rawHolds(const std::string& key) { return osmLuaProcessing->Holds(key); } -const std::string& rawFind(const std::string& key) { return osmLuaProcessing->Find(key); } +bool rawHolds(const KnownTagKey& key) { return key.found; } +const std::string& rawFind(const KnownTagKey& key) { + if (key.found) + return *(osmLuaProcessing->currentTags->getValue(key.index)); + + return EMPTY_STRING; +} std::vector rawFindIntersecting(const std::string &layerName) { return osmLuaProcessing->FindIntersecting(layerName); } bool rawIntersects(const std::string& layerName) { return osmLuaProcessing->Intersects(layerName); } std::vector rawFindCovering(const std::string& layerName) { return osmLuaProcessing->FindCovering(layerName); } @@ -34,7 +81,6 @@ std::vector rawCentroid() { return osmLuaProcessing->Centroid(); } bool supportsRemappingShapefiles = false; -const std::string EMPTY_STRING = ""; int lua_error_handler(int errCode, const char *errMessage) { @@ -151,18 +197,6 @@ string OsmLuaProcessing::Id() const { return to_string(originalOsmID); } -// Check if there's a value for a given key -bool OsmLuaProcessing::Holds(const string& key) const { - return currentTags->getTag(key) != nullptr; -} - -// Get an OSM tag for a given key (or return empty string if none) -const string& OsmLuaProcessing::Find(const string& key) const { - auto it = currentTags->getTag(key); - if(it == nullptr) return EMPTY_STRING; - return *it; -} - // ---- Spatial queries called from Lua vector OsmLuaProcessing::FindIntersecting(const string &layerName) { diff --git a/src/tag_map.cpp b/src/tag_map.cpp index 00962e8e..1e07b2cc 100644 --- a/src/tag_map.cpp +++ b/src/tag_map.cpp @@ -29,6 +29,19 @@ const std::size_t hashString(const std::string& str) { return hash; } +const std::size_t hashString(const char* str, size_t size) { + // This is a pretty crappy hash function in terms of bit + // avalanching and distribution of output values. + // + // But it's very good in terms of speed, which turns out + // to be the important measure. + std::size_t hash = size; + if (hash >= 4) + hash ^= *(uint32_t*)str; + + return hash; +} + uint32_t TagMap::ensureString( std::vector>& vector, const std::string& value @@ -77,6 +90,28 @@ const std::string* TagMap::getTag(const std::string& key) const { return nullptr; } +int64_t TagMap::getTag(const char* key, size_t size) const { + // Return -1 if key not found, else return its keyLoc. + std::size_t hash = hashString(key, size); + + const uint16_t shard = hash % keys.size(); + for (int i = 0; i < keys[shard].size(); i++) { + const std::string& candidate = *keys[shard][i]; + if (candidate.size() != size) + continue; + + if (memcmp(candidate.data(), key, size) == 0) + return shard << 16 | i; + } + + return -1; +} + +const std::string* TagMap::getValue(uint32_t keyLoc) const { + const uint32_t valueLoc = key2value[keyLoc >> 16][keyLoc & 0xFFFF]; + return values[valueLoc >> 16][valueLoc & 0xFFFF]; +} + boost::container::flat_map TagMap::exportToBoostMap() const { boost::container::flat_map rv; diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index 5c2e19df..125f6c89 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -392,7 +392,7 @@ int main(int argc, char* argv[]) { } // ---- Write out data - return 0; // TODO +// return 0; // TODO // If mapsplit, read list of tiles available unsigned runs=1; From 3922170c1aed2f440ee96696313cfb19636730e5 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 3 Dec 2023 18:35:36 -0500 Subject: [PATCH 04/81] tweak names --- include/tag_map.h | 2 +- src/osm_lua_processing.cpp | 2 +- src/tag_map.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/tag_map.h b/include/tag_map.h index af45e76d..9326a878 100644 --- a/include/tag_map.h +++ b/include/tag_map.h @@ -33,7 +33,7 @@ class TagMap { // Return -1 if key not found, else return its keyLoc. int64_t getTag(const char* key, size_t size) const; - const std::string* getValue(uint32_t keyLoc) const; + const std::string* getValueFromKey(uint32_t keyLoc) const; boost::container::flat_map exportToBoostMap() const; diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index cb5cb889..b7760896 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -56,7 +56,7 @@ std::string rawId() { return osmLuaProcessing->Id(); } bool rawHolds(const KnownTagKey& key) { return key.found; } const std::string& rawFind(const KnownTagKey& key) { if (key.found) - return *(osmLuaProcessing->currentTags->getValue(key.index)); + return *(osmLuaProcessing->currentTags->getValueFromKey(key.index)); return EMPTY_STRING; } diff --git a/src/tag_map.cpp b/src/tag_map.cpp index 1e07b2cc..0d7a58f8 100644 --- a/src/tag_map.cpp +++ b/src/tag_map.cpp @@ -107,7 +107,7 @@ int64_t TagMap::getTag(const char* key, size_t size) const { return -1; } -const std::string* TagMap::getValue(uint32_t keyLoc) const { +const std::string* TagMap::getValueFromKey(uint32_t keyLoc) const { const uint32_t valueLoc = key2value[keyLoc >> 16][keyLoc & 0xFFFF]; return values[valueLoc >> 16][valueLoc & 0xFFFF]; } From 13b3465f1c80052aa2d622e3915af08b8c5eae9a Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 3 Dec 2023 19:06:38 -0500 Subject: [PATCH 05/81] avoid malloc for Attribute with long strings After: ``` real 1m8.124s user 16m6.620s sys 0m16.808s ``` Looks like we're solidly into diminishing returns at this point. --- include/osm_lua_processing.h | 19 ++++++++--- include/tag_map.h | 6 +++- src/osm_lua_processing.cpp | 66 ++++++++++++++++++++++++++++++------ src/tag_map.cpp | 23 ++++++++++++- src/tilemaker.cpp | 1 - 5 files changed, 97 insertions(+), 18 deletions(-) diff --git a/include/osm_lua_processing.h b/include/osm_lua_processing.h index c74e8800..189e7e6d 100644 --- a/include/osm_lua_processing.h +++ b/include/osm_lua_processing.h @@ -33,6 +33,20 @@ extern bool verbose; class AttributeStore; class AttributeSet; +// A string, which might be in `currentTags` as a value. If Lua +// code refers to an absent value, it'll fallback to passing +// it as a std::string. +// +// The intent is that Attribute("name", Find("name")) is a common +// pattern, and we ought to avoid marshalling a string back and +// forth from C++ to Lua when possible. +struct PossiblyKnownTagValue { + bool found; + uint32_t index; + std::string fallback; +}; + + /** \brief OsmLuaProcessing - converts OSM objects into OutputObjects. @@ -151,11 +165,8 @@ class OsmLuaProcessing { void LayerAsCentroid(const std::string &layerName); // Set attributes in a vector tile's Attributes table - void Attribute(const std::string &key, const std::string &val); - void AttributeWithMinZoom(const std::string &key, const std::string &val, const char minzoom); - void AttributeNumeric(const std::string &key, const float val); + void AttributeWithMinZoom(const std::string &key, const PossiblyKnownTagValue& val, const char minzoom); void AttributeNumericWithMinZoom(const std::string &key, const float val, const char minzoom); - void AttributeBoolean(const std::string &key, const bool val); void AttributeBooleanWithMinZoom(const std::string &key, const bool val, const char minzoom); void MinZoom(const double z); void ZOrder(const double z); diff --git a/include/tag_map.h b/include/tag_map.h index 9326a878..0fef12e9 100644 --- a/include/tag_map.h +++ b/include/tag_map.h @@ -31,9 +31,13 @@ class TagMap { const std::string* getTag(const std::string& key) const; // Return -1 if key not found, else return its keyLoc. - int64_t getTag(const char* key, size_t size) const; + int64_t getKey(const char* key, size_t size) const; + + // Return -1 if value not found, else return its keyLoc. + int64_t getValue(const char* key, size_t size) const; const std::string* getValueFromKey(uint32_t keyLoc) const; + const std::string* getValue(uint32_t valueLoc) const; boost::container::flat_map exportToBoostMap() const; diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index b7760896..0d181f87 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -36,7 +36,7 @@ template<> struct kaguya::lua_type_traits { size_t size = 0; const char* buffer = lua_tolstring(l, index, &size); - int64_t tagLoc = osmLuaProcessing->currentTags->getTag(buffer, size); + int64_t tagLoc = osmLuaProcessing->currentTags->getKey(buffer, size); if (tagLoc >= 0) { rv.found = true; @@ -52,6 +52,49 @@ template<> struct kaguya::lua_type_traits { } }; +template<> struct kaguya::lua_type_traits { + typedef PossiblyKnownTagValue get_type; + typedef const PossiblyKnownTagValue& push_type; + + static bool strictCheckType(lua_State* l, int index) + { + return lua_type(l, index) == LUA_TSTRING; + } + static bool checkType(lua_State* l, int index) + { + return lua_isstring(l, index) != 0; + } + static get_type get(lua_State* l, int index) + { + PossiblyKnownTagValue rv = { false, 0 }; + size_t size = 0; + const char* buffer = lua_tolstring(l, index, &size); + + // For long strings where we might need to do a malloc, see if we + // can instead pass a pointer to a value from this object's tag + // map. + // + // 15 is the threshold where gcc no longer applies the small string + // optimization. + if (size > 15) { + int64_t tagLoc = osmLuaProcessing->currentTags->getValue(buffer, size); + + if (tagLoc >= 0) { + rv.found = true; + rv.index = tagLoc; + return rv; + } + } + + rv.fallback = std::string(buffer, size); + return rv; + } + static int push(lua_State* l, push_type s) + { + throw std::runtime_error("Lua code doesn't know how to use PossiblyKnownTagValue"); + } +}; + std::string rawId() { return osmLuaProcessing->Id(); } bool rawHolds(const KnownTagKey& key) { return key.found; } const std::string& rawFind(const KnownTagKey& key) { @@ -128,15 +171,15 @@ OsmLuaProcessing::OsmLuaProcessing( luaState["Layer"] = &rawLayer; luaState["LayerAsCentroid"] = &rawLayerAsCentroid; luaState["Attribute"] = kaguya::overload( - [](const std::string &key, const std::string& val) { osmLuaProcessing->Attribute(key, val); }, - [](const std::string &key, const std::string& val, const char minzoom) { osmLuaProcessing->AttributeWithMinZoom(key, val, minzoom); } + [](const std::string &key, const PossiblyKnownTagValue& val) { osmLuaProcessing->AttributeWithMinZoom(key, val, 0); }, + [](const std::string &key, const PossiblyKnownTagValue& val, const char minzoom) { osmLuaProcessing->AttributeWithMinZoom(key, val, minzoom); } ); luaState["AttributeNumeric"] = kaguya::overload( - [](const std::string &key, const float val) { osmLuaProcessing->AttributeNumeric(key, val); }, + [](const std::string &key, const float val) { osmLuaProcessing->AttributeNumericWithMinZoom(key, val, 0); }, [](const std::string &key, const float val, const char minzoom) { osmLuaProcessing->AttributeNumericWithMinZoom(key, val, minzoom); } ); luaState["AttributeBoolean"] = kaguya::overload( - [](const std::string &key, const bool val) { osmLuaProcessing->AttributeBoolean(key, val); }, + [](const std::string &key, const bool val) { osmLuaProcessing->AttributeBooleanWithMinZoom(key, val, 0); }, [](const std::string &key, const bool val, const char minzoom) { osmLuaProcessing->AttributeBooleanWithMinZoom(key, val, minzoom); } ); @@ -541,22 +584,23 @@ void OsmLuaProcessing::Accept() { } // Set attributes in a vector tile's Attributes table -void OsmLuaProcessing::Attribute(const string &key, const string &val) { AttributeWithMinZoom(key,val,0); } -void OsmLuaProcessing::AttributeWithMinZoom(const string &key, const string &val, const char minzoom) { - if (val.size()==0) { return; } // don't set empty strings +void OsmLuaProcessing::AttributeWithMinZoom(const string &key, const PossiblyKnownTagValue& val, const char minzoom) { + const std::string* str = &val.fallback; + if (val.found) + str = currentTags->getValue(val.index); + + if (str->size()==0) { return; } // don't set empty strings if (outputs.size()==0) { ProcessingError("Can't add Attribute if no Layer set"); return; } - attributeStore.addAttribute(outputs.back().second, key, val, minzoom); + attributeStore.addAttribute(outputs.back().second, key, *str, minzoom); setVectorLayerMetadata(outputs.back().first.layer, key, 0); } -void OsmLuaProcessing::AttributeNumeric(const string &key, const float val) { AttributeNumericWithMinZoom(key,val,0); } void OsmLuaProcessing::AttributeNumericWithMinZoom(const string &key, const float val, const char minzoom) { if (outputs.size()==0) { ProcessingError("Can't add Attribute if no Layer set"); return; } attributeStore.addAttribute(outputs.back().second, key, val, minzoom); setVectorLayerMetadata(outputs.back().first.layer, key, 1); } -void OsmLuaProcessing::AttributeBoolean(const string &key, const bool val) { AttributeBooleanWithMinZoom(key,val,0); } void OsmLuaProcessing::AttributeBooleanWithMinZoom(const string &key, const bool val, const char minzoom) { if (outputs.size()==0) { ProcessingError("Can't add Attribute if no Layer set"); return; } attributeStore.addAttribute(outputs.back().second, key, val, minzoom); diff --git a/src/tag_map.cpp b/src/tag_map.cpp index 0d7a58f8..34f35948 100644 --- a/src/tag_map.cpp +++ b/src/tag_map.cpp @@ -90,7 +90,7 @@ const std::string* TagMap::getTag(const std::string& key) const { return nullptr; } -int64_t TagMap::getTag(const char* key, size_t size) const { +int64_t TagMap::getKey(const char* key, size_t size) const { // Return -1 if key not found, else return its keyLoc. std::size_t hash = hashString(key, size); @@ -107,11 +107,32 @@ int64_t TagMap::getTag(const char* key, size_t size) const { return -1; } +int64_t TagMap::getValue(const char* value, size_t size) const { + // Return -1 if value not found, else return its valueLoc. + std::size_t hash = hashString(value, size); + + const uint16_t shard = hash % values.size(); + for (int i = 0; i < values[shard].size(); i++) { + const std::string& candidate = *values[shard][i]; + if (candidate.size() != size) + continue; + + if (memcmp(candidate.data(), value, size) == 0) + return shard << 16 | i; + } + + return -1; +} + const std::string* TagMap::getValueFromKey(uint32_t keyLoc) const { const uint32_t valueLoc = key2value[keyLoc >> 16][keyLoc & 0xFFFF]; return values[valueLoc >> 16][valueLoc & 0xFFFF]; } +const std::string* TagMap::getValue(uint32_t valueLoc) const { + return values[valueLoc >> 16][valueLoc & 0xFFFF]; +} + boost::container::flat_map TagMap::exportToBoostMap() const { boost::container::flat_map rv; diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index 125f6c89..9643f226 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -392,7 +392,6 @@ int main(int argc, char* argv[]) { } // ---- Write out data -// return 0; // TODO // If mapsplit, read list of tiles available unsigned runs=1; From 8c179d19884e67d16009cc45916b47b0f192c91b Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 3 Dec 2023 19:11:31 -0500 Subject: [PATCH 06/81] fix make --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b54f6b8..5f54b4ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,7 @@ file(GLOB tilemaker_src_files src/pbf_blocks.cpp src/read_shp.cpp src/shp_mem_tiles.cpp + src/tag_map.cpp src/tilemaker.cpp src/write_geometry.cpp ) From 5f30a30eb34ba2184c90f6a2084bbfbdcb30a65d Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Fri, 15 Dec 2023 18:28:11 -0500 Subject: [PATCH 07/81] move OutputObjects to mmap store For the planet, we need 1.3B output objects, 12 bytes per, so ~15GB of RAM. --- include/tile_data.h | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/include/tile_data.h b/include/tile_data.h index 814b53ce..c7b808ab 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -9,6 +9,7 @@ #include #include "output_object.h" #include "clip_cache.h" +#include "mmap_allocator.h" typedef std::vector SourceList; @@ -47,10 +48,10 @@ struct OutputObjectXYID { template void finalizeObjects( const size_t& threadNum, const unsigned int& baseZoom, - typename std::vector>::iterator begin, - typename std::vector>::iterator end + typename std::vector>>::iterator begin, + typename std::vector>>::iterator end ) { - for (typename std::vector>::iterator it = begin; it != end; it++) { + for (auto it = begin; it != end; it++) { if (it->size() == 0) continue; @@ -108,7 +109,7 @@ template void finalizeObjects( template void collectTilesWithObjectsAtZoomTemplate( const unsigned int& baseZoom, - const typename std::vector>::iterator objects, + const typename std::vector>>::iterator objects, const size_t size, const unsigned int zoom, TileCoordinatesSet& output @@ -150,7 +151,7 @@ inline OutputObjectID outputObjectWithId(const OutputObjectXYI template void collectObjectsForTileTemplate( const unsigned int& baseZoom, - typename std::vector>::iterator objects, + typename std::vector>>::iterator objects, size_t iStart, size_t iEnd, unsigned int zoom, @@ -292,8 +293,8 @@ class TileDataSource { // // If config.include_ids is true, objectsWithIds will be populated. // Otherwise, objects. - std::vector> objects; - std::vector> objectsWithIds; + std::vector>> objects; + std::vector>> objectsWithIds; // rtree index of large objects using oo_rtree_param_type = boost::geometry::index::quadratic<128>; From b9187693992e130f6037314f153947ca8002a831 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Fri, 15 Dec 2023 18:45:27 -0500 Subject: [PATCH 08/81] treat objects at low zoom specially For GB, ~0.3% of objects are visible at low zooms. I noticed in previous planet runs that fetching the objects for tiles in the low zooms was quite slow - I think it's because we're scanning 1.3B objects each time, only to discard most of them. Now we'll only be scanning ~4M objects per tile, which is still an absurd number, but should mitigate most of the speed issue without having to properly index things. This will also help us maintain performance for memory-constrained users, as we won't be scanning all 15GB of data on disk, just a smaller ~45MB chunk. --- include/tile_data.h | 10 ++++++++-- src/tile_data.cpp | 12 ++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/include/tile_data.h b/include/tile_data.h index c7b808ab..13d61dbe 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -49,7 +49,8 @@ template void finalizeObjects( const size_t& threadNum, const unsigned int& baseZoom, typename std::vector>>::iterator begin, - typename std::vector>>::iterator end + typename std::vector>>::iterator end, + typename std::vector>>& lowZoom ) { for (auto it = begin; it != end; it++) { if (it->size() == 0) @@ -57,6 +58,10 @@ template void finalizeObjects( it->shrink_to_fit(); + for (auto objectIt = it->begin(); objectIt != it->end(); objectIt++) + if (objectIt->oo.minZoom < CLUSTER_ZOOM) + lowZoom[0].push_back(*objectIt); + // If the user is doing a a small extract, there are few populated // entries in `object`. // @@ -103,7 +108,6 @@ template void finalizeObjects( }, threadNum ); - } } @@ -294,7 +298,9 @@ class TileDataSource { // If config.include_ids is true, objectsWithIds will be populated. // Otherwise, objects. std::vector>> objects; + std::vector>> lowZoomObjects; std::vector>> objectsWithIds; + std::vector>> lowZoomObjectsWithIds; // rtree index of large objects using oo_rtree_param_type = boost::geometry::index::quadratic<128>; diff --git a/src/tile_data.cpp b/src/tile_data.cpp index 696ed333..234288fd 100644 --- a/src/tile_data.cpp +++ b/src/tile_data.cpp @@ -47,7 +47,9 @@ TileDataSource::TileDataSource(size_t threadNum, unsigned int baseZoom, bool inc z6OffsetDivisor(baseZoom >= CLUSTER_ZOOM ? (1 << (baseZoom - CLUSTER_ZOOM)) : 1), objectsMutex(threadNum * 4), objects(CLUSTER_ZOOM_AREA), + lowZoomObjects(1), objectsWithIds(CLUSTER_ZOOM_AREA), + lowZoomObjectsWithIds(1), baseZoom(baseZoom), pointStores(threadNum), linestringStores(threadNum), @@ -72,8 +74,9 @@ TileDataSource::TileDataSource(size_t threadNum, unsigned int baseZoom, bool inc } void TileDataSource::finalize(size_t threadNum) { - finalizeObjects(threadNum, baseZoom, objects.begin(), objects.end()); - finalizeObjects(threadNum, baseZoom, objectsWithIds.begin(), objectsWithIds.end()); + finalizeObjects(threadNum, baseZoom, objects.begin(), objects.end(), lowZoomObjects); + finalizeObjects(threadNum, baseZoom, objectsWithIds.begin(), objectsWithIds.end(), lowZoomObjectsWithIds); + } void TileDataSource::addObjectToSmallIndex(const TileCoordinates& index, const OutputObject& oo, uint64_t id) { @@ -139,6 +142,11 @@ void TileDataSource::collectObjectsForTile( TileCoordinates dstIndex, std::vector& output ) { + if (zoom < CLUSTER_ZOOM) { + collectObjectsForTileTemplate(baseZoom, lowZoomObjects.begin(), 0, 1, zoom, dstIndex, output); + collectObjectsForTileTemplate(baseZoom, lowZoomObjectsWithIds.begin(), 0, 1, zoom, dstIndex, output); + } + size_t iStart = 0; size_t iEnd = objects.size(); From d7caf1024bc361bcc4a5f836c255e44211cf53d6 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 10:46:05 -0500 Subject: [PATCH 09/81] make more explicit that this is unexpected --- src/tile_data.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/tile_data.cpp b/src/tile_data.cpp index 234288fd..a50ddfea 100644 --- a/src/tile_data.cpp +++ b/src/tile_data.cpp @@ -196,11 +196,7 @@ Geometry TileDataSource::buildWayGeometry(OutputGeometryType const geomType, NodeID const objectID, const TileBbox &bbox) { switch(geomType) { case POINT_: { - auto p = retrievePoint(objectID); - if (geom::within(p, bbox.clippingBox)) { - return p; - } - return MultiLinestring(); + throw std::runtime_error("unexpected geomType in buildWayGeometry"); } case LINESTRING_: { From 8dff5bf1c1f1af7298b85d482ee4e88d14e09197 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 10:46:27 -0500 Subject: [PATCH 10/81] extend --materialize-geometries to nodes For Points stored via Layer(...) calls, store the node ID in the OSM store, unless `--materialize-geometries` is present. This saves ~200MB of RAM for North America, so perhaps 1 GB for the planet if NA has similar characteristics as the planet. Also fix the OSM_ID(...) macro - it was lopping off many more bits than needed, due to some previous experiments. Now that we want to track nodes, we need at least 34 bits. This may pose a problem down the road when we try to address thrashing. The mechanism I hoped to use was to divide the OSM stores into multiple stores covering different low zoom tiles. Ideally, we'd be able to recall which store to look in -- but we only have 36 bits, we need 34 to store the Node ID, so that leaves us with 1.5 bits => can divide into 3 stores. Since the node store for the planet is 44GB, dividing into 3 stores doesn't give us very much headroom on a 32 GB box. Ah well, we can sort this out later. --- include/osm_mem_tiles.h | 8 +++++++- include/tile_data.h | 2 +- src/osm_lua_processing.cpp | 4 +++- src/osm_mem_tiles.cpp | 21 +++++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/include/osm_mem_tiles.h b/include/osm_mem_tiles.h index a6266ea3..74aeb18f 100644 --- a/include/osm_mem_tiles.h +++ b/include/osm_mem_tiles.h @@ -6,10 +6,15 @@ #include "osm_store.h" #include "geometry_cache.h" +// NB: Currently, USE_NODE_STORE and USE_WAY_STORE are equivalent. +// If we permit LayerAsCentroid to be generated from the OSM stores, +// this will have to change. #define OSM_THRESHOLD (1ull << 35) +#define USE_NODE_STORE (1ull << 35) +#define IS_NODE(x) (((x) >> 35) == (USE_NODE_STORE >> 35)) #define USE_WAY_STORE (1ull << 35) #define IS_WAY(x) (((x) >> 35) == (USE_WAY_STORE >> 35)) -#define OSM_ID(x) ((x) & 0b111111111111111111111111111111111) +#define OSM_ID(x) ((x) & 0b11111111111111111111111111111111111) class NodeStore; class WayStore; @@ -37,6 +42,7 @@ class OsmMemTiles : public TileDataSource { const NodeID objectID, const TileBbox &bbox ) override; + LatpLon buildNodeGeometry(OutputGeometryType const geomType, NodeID const objectID, const TileBbox &bbox) const override; void Clear(); diff --git a/include/tile_data.h b/include/tile_data.h index 13d61dbe..e02a0255 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -362,7 +362,7 @@ class TileDataSource { ); virtual Geometry buildWayGeometry(OutputGeometryType const geomType, NodeID const objectID, const TileBbox &bbox); - LatpLon buildNodeGeometry(OutputGeometryType const geomType, NodeID const objectID, const TileBbox &bbox) const; + virtual LatpLon buildNodeGeometry(OutputGeometryType const geomType, NodeID const objectID, const TileBbox &bbox) const; void open() { // Put something at index 0 of all stores so that 0 can be used diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index a1bc2536..a90c8b6a 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -350,7 +350,9 @@ void OsmLuaProcessing::Layer(const string &layerName, bool area) { if(CorrectGeometry(p) == CorrectGeometryResult::Invalid) return; - NodeID id = osmMemTiles.storePoint(p); + NodeID id = USE_NODE_STORE | originalOsmID; + if (materializeGeometries) + id = osmMemTiles.storePoint(p); OutputObject oo(geomType, layers.layerMap[layerName], id, 0, layerMinZoom); outputs.push_back(std::make_pair(std::move(oo), attributes)); return; diff --git a/src/osm_mem_tiles.cpp b/src/osm_mem_tiles.cpp index f5527d0e..5cfc3c3d 100644 --- a/src/osm_mem_tiles.cpp +++ b/src/osm_mem_tiles.cpp @@ -18,6 +18,27 @@ OsmMemTiles::OsmMemTiles( { } +LatpLon OsmMemTiles::buildNodeGeometry( + OutputGeometryType const geomType, + NodeID const objectID, + const TileBbox &bbox +) const { + if (objectID < OSM_THRESHOLD) { + return TileDataSource::buildNodeGeometry(geomType, objectID, bbox); + } + + switch(geomType) { + case POINT_: { + return nodeStore.at(OSM_ID(objectID)); + } + + default: + break; + } + + throw std::runtime_error("Geometry type is not point"); +} + Geometry OsmMemTiles::buildWayGeometry( const OutputGeometryType geomType, const NodeID objectID, From b86fddc307e173ca03edac9b229318f0a3660ace Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 12:44:49 -0500 Subject: [PATCH 11/81] rejig AttributePair layout On g++, this reduces the size from 48 bytes to 34 bytes. There aren't _that_ many attribute pairs, even on the planet scale, but this plus a better encoding of string attributes might save us ~2GB at the planet level, which is meaningful for a 32GB box --- include/attribute_store.h | 65 ++++++++++++++++++++++++++++++++------- include/output_object.h | 3 -- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/include/attribute_store.h b/include/attribute_store.h index ad1aa4e1..9d606139 100644 --- a/include/attribute_store.h +++ b/include/attribute_store.h @@ -39,17 +39,20 @@ class AttributeKeyStore { std::map keys2index; }; -enum class AttributePairType: char { False = 0, True = 1, Float = 2, String = 3 }; +enum class AttributePairType: char { Bool = 0, Float = 1, String = 2 }; // AttributePair is a key/value pair (with minzoom) +#pragma pack(push, 1) struct AttributePair { - std::string stringValue_; - float floatValue_; - short keyIndex; - char minzoom; - AttributePairType valueType; + short keyIndex : 9; + AttributePairType valueType : 3; + char minzoom : 4; + union { + float floatValue_; + std::string stringValue_; + }; AttributePair(uint32_t keyIndex, bool value, char minzoom) - : keyIndex(keyIndex), valueType(value ? AttributePairType::True : AttributePairType::False), minzoom(minzoom) + : keyIndex(keyIndex), valueType(AttributePairType::Bool), minzoom(minzoom), floatValue_(value ? 1 : 0) { } AttributePair(uint32_t keyIndex, const std::string& value, char minzoom) @@ -57,16 +60,55 @@ struct AttributePair { { } AttributePair(uint32_t keyIndex, float value, char minzoom) - : keyIndex(keyIndex), valueType(AttributePairType::Float), floatValue_(value), minzoom(minzoom) + : keyIndex(keyIndex), valueType(AttributePairType::Float), minzoom(minzoom), floatValue_(value) { } + ~AttributePair() { + if (valueType == AttributePairType::Bool || valueType == AttributePairType::Float) + return; + + stringValue_.~basic_string(); + } + + AttributePair(const AttributePair& other): + keyIndex(other.keyIndex), valueType(other.valueType), minzoom(other.minzoom) + { + if (valueType == AttributePairType::Bool || valueType == AttributePairType::Float) { + floatValue_ = other.floatValue_; + return; + } + + new (&stringValue_) std::string; + stringValue_ = other.stringValue_; + } + + AttributePair& operator=(const AttributePair& other) { + if (!(valueType == AttributePairType::Bool || valueType == AttributePairType::Float)) { + stringValue_.~basic_string(); + } + + keyIndex = other.keyIndex; + valueType = other.valueType; + minzoom = other.minzoom; + + if (valueType == AttributePairType::Bool || valueType == AttributePairType::Float) { + floatValue_ = other.floatValue_; + return *this; + } + + new (&stringValue_) std::string; + stringValue_ = other.stringValue_; + + return *this; + } + bool operator==(const AttributePair &other) const { if (minzoom!=other.minzoom || keyIndex!=other.keyIndex || valueType!=other.valueType) return false; if (valueType == AttributePairType::String) return stringValue_ == other.stringValue_; - if (valueType == AttributePairType::Float) + if (valueType == AttributePairType::Float || valueType == AttributePairType::Bool) return floatValue_ == other.floatValue_; return true; @@ -74,11 +116,11 @@ struct AttributePair { bool hasStringValue() const { return valueType == AttributePairType::String; } bool hasFloatValue() const { return valueType == AttributePairType::Float; } - bool hasBoolValue() const { return valueType == AttributePairType::True || valueType == AttributePairType::False; }; + bool hasBoolValue() const { return valueType == AttributePairType::Bool; } const std::string& stringValue() const { return stringValue_; } float floatValue() const { return floatValue_; } - bool boolValue() const { return valueType == AttributePairType::True; } + bool boolValue() const { return floatValue_; } static bool isHot(const AttributePair& pair, const std::string& keyName) { // Is this pair a candidate for the hot pool? @@ -137,6 +179,7 @@ struct AttributePair { return rv; } }; +#pragma pack(pop) // We shard the cold pools to reduce the odds of lock contention on diff --git a/include/output_object.h b/include/output_object.h index 3d2d862e..385fd46d 100644 --- a/include/output_object.h +++ b/include/output_object.h @@ -22,9 +22,6 @@ std::ostream& operator<<(std::ostream& os, OutputGeometryType geomType); /** * \brief OutputObject - any object (node, linestring, polygon) to be outputted to tiles - - * Possible future improvements to save memory: - * - use a global dictionary for attribute key/values */ #pragma pack(push, 4) class OutputObject { From a54938e3db84658af148a633d8ea077357642b9d Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 14:48:42 -0500 Subject: [PATCH 12/81] fix initialization order warning --- src/mmap_allocator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mmap_allocator.cpp b/src/mmap_allocator.cpp index dc71f687..2b5e26fd 100644 --- a/src/mmap_allocator.cpp +++ b/src/mmap_allocator.cpp @@ -79,10 +79,10 @@ thread_local mmap_shm_ptr mmap_shm_thread_region_ptr; std::mutex mmap_allocator_mutex; mmap_file::mmap_file(std::string const &filename, std::size_t offset) - : mapping(filename.c_str(), boost::interprocess::read_write) + : filename(filename) + , mapping(filename.c_str(), boost::interprocess::read_write) , region(mapping, boost::interprocess::read_write) , buffer(boost::interprocess::create_only, reinterpret_cast(region.get_address()) + offset, region.get_size() - offset) - , filename(filename) { } mmap_file::~mmap_file() From fa5a2bf858ba9733a3db4724a847c9ad3fd8f0e6 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 14:49:08 -0500 Subject: [PATCH 13/81] add PooledString Not used by anything yet. Given Tilemaker's limited needs, we can get away with a stripped-down string class that is less flexible than std::string, in exchange for memory savings. The key benefits - 16 bytes, not 32 bytes (g++) or 24 bytes (clang). When it does allocate (for strings longer than 15 bytes), it allocates from a pool so there's less per-allocation overhead. --- CMakeLists.txt | 1 + Makefile | 13 +++-- include/pooled_string.h | 45 +++++++++++++++++ src/attribute_store.cpp | 2 +- src/pooled_string.cpp | 98 +++++++++++++++++++++++++++++++++++++ test/pooled_string.test.cpp | 34 +++++++++++++ 6 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 include/pooled_string.h create mode 100644 src/pooled_string.cpp create mode 100644 test/pooled_string.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 203e68e8..d69e61ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,7 @@ file(GLOB tilemaker_src_files src/osm_store.cpp src/output_object.cpp src/pbf_blocks.cpp + src/pooled_string.cpp src/read_pbf.cpp src/read_shp.cpp src/shared_data.cpp diff --git a/Makefile b/Makefile index 234dca6a..84fa084c 100644 --- a/Makefile +++ b/Makefile @@ -111,6 +111,7 @@ tilemaker: \ src/osm_store.o \ src/output_object.o \ src/pbf_blocks.o \ + src/pooled_string.o \ src/read_pbf.o \ src/read_shp.o \ src/shared_data.o \ @@ -124,7 +125,13 @@ tilemaker: \ src/write_geometry.o $(CXX) $(CXXFLAGS) -o tilemaker $^ $(INC) $(LIB) $(LDFLAGS) -test: test_sorted_way_store +test: test_pooled_string test_sorted_way_store + +test_pooled_string: \ + src/mmap_allocator.o \ + src/pooled_string.o \ + test/pooled_string.test.o + $(CXX) $(CXXFLAGS) -o test.pooled_string $^ $(INC) $(LIB) $(LDFLAGS) && ./test.pooled_string test_sorted_way_store: \ src/external/streamvbyte_decode.o \ @@ -132,7 +139,7 @@ test_sorted_way_store: \ src/external/streamvbyte_zigzag.o \ src/mmap_allocator.o \ src/sorted_way_store.o \ - src/sorted_way_store.test.o + test/sorted_way_store.test.o $(CXX) $(CXXFLAGS) -o test.sorted_way_store $^ $(INC) $(LIB) $(LDFLAGS) && ./test.sorted_way_store @@ -152,6 +159,6 @@ install: install docs/man/tilemaker.1 ${DESTDIR}${MANPREFIX}/man1/ clean: - rm -f tilemaker src/*.o src/external/*.o include/*.o include/*.pb.h + rm -f tilemaker src/*.o src/external/*.o include/*.o include/*.pb.h test/*.o .PHONY: install diff --git a/include/pooled_string.h b/include/pooled_string.h new file mode 100644 index 00000000..2fbeca37 --- /dev/null +++ b/include/pooled_string.h @@ -0,0 +1,45 @@ +#ifndef _POOLED_STRING_H +#define _POOLED_STRING_H + +// std::string allows the allocated size to differ from the used size, +// which means it needs an extra pointer. It also supports large strings. +// +// Our use case does not require this: we have immutable strings and always +// know their exact size, which fit in 64K. +// +// Further, g++'s implementation of std::string is inefficient - it takes 32 +// bytes (vs clang's 24 bytes), while only allowing a small-string optimization +// for strings of length 15 or less. +// +// std::string also needs to be able to free its allocated memory -- in our case, +// we're fine with the memory living until the process dies. +// +// Instead, we implemented `PooledString`. It has a size of 16 bytes, and a small +// string optimization for strings <= 15 bytes. (We will separately teach +// AttributePair to encode Latin-character strings more efficiently, so that many +// strings of size 24 or less fit in 15 bytes.) +// +// If it needs to allocate memory, it does so from a shared pool. It is unable +// to free the memory once allocated. + +#include +#include + +namespace PooledStringNS { + class PooledString { + public: + PooledString(const std::string& str); + size_t size() const; + bool operator==(const PooledString& other) const; + bool operator!=(const PooledString& other) const; + std::string toString() const; + + private: + // 0..3 is index into table, 4..5 is offset, 6..7 is length + uint8_t storage[16]; + }; +} + +using PooledString = PooledStringNS::PooledString; + +#endif diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index f4f9f299..83acc1ac 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -92,7 +92,7 @@ uint32_t AttributePairStore::addPair(const AttributePair& pair, bool isHot) { // Not found, ensure our local map is up-to-date for future calls, // and fall through to the main map. // - // Note that we can read `hotShard` without a lock + // Note that we can read `hotShard` without a lock, its size is fixed while (tlsHotShardSize < hotShardSize.load()) { tlsHotShardSize++; tlsHotShardMap[&hotShard[tlsHotShardSize]] = tlsHotShardSize; diff --git a/src/pooled_string.cpp b/src/pooled_string.cpp new file mode 100644 index 00000000..031a5c06 --- /dev/null +++ b/src/pooled_string.cpp @@ -0,0 +1,98 @@ +#include "pooled_string.h" +#include +#include + +namespace PooledStringNS { + std::vector tables; + std::mutex mutex; + + // Each thread has its own string table, we only take a lock + // to push a new table onto the vector. + thread_local int64_t tableIndex = -1; + thread_local int64_t spaceLeft = -1; +} + +PooledString::PooledString(const std::string& str) { + if (str.size() >= 65536) + throw std::runtime_error("cannot store string longer than 64K"); + + if (str.size() <= 15) { + storage[0] = str.size(); + memcpy(storage + 1, str.data(), str.size()); + memset(storage + 1 + str.size(), 0, 16 - 1 - str.size()); + } else { + memset(storage + 8, 0, 8); + storage[0] = 1 << 7; + + if (spaceLeft < 0 || spaceLeft < str.size()) { + std::lock_guard lock(mutex); + spaceLeft = 65536; + char* buffer = (char*)malloc(spaceLeft); + if (buffer == 0) + throw std::runtime_error("PooledString could not malloc"); + tables.push_back(buffer); + tableIndex = tables.size() - 1; + } + + storage[1] = tableIndex >> 16; + storage[2] = tableIndex >> 8; + storage[3] = tableIndex; + + uint16_t offset = 65536 - spaceLeft; + storage[4] = offset >> 8; + storage[5] = offset; + + uint16_t length = str.size(); + storage[6] = length >> 8; + storage[7] = length; + + memcpy(tables[tableIndex] + offset, str.data(), str.size()); + + spaceLeft -= str.size(); + } +} + +bool PooledStringNS::PooledString::operator==(const PooledString& other) const { + // NOTE: We have surprising equality semantics! + // + // For short strings, you are equal if the strings are equal. + // + // For large strings, you are equal if you use the same heap memory locations. + // This implies that someone outside of PooledString is managing pooling! In our + // case, it is the responsibility of AttributePairStore. + return memcmp(storage, other.storage, 16) == 0; +} + +bool PooledStringNS::PooledString::operator!=(const PooledString& other) const { + return !(*this == other); +} + +size_t PooledStringNS::PooledString::size() const { + // If the uppermost bit is set, we're in heap. + if (storage[0] >> 7) { + uint16_t length = (storage[6] << 8) + storage[7]; + return length; + } + + // Otherwise it's stored in the lower 7 bits of the highest byte. + return storage[0] & 0b01111111; +} + +std::string PooledStringNS::PooledString::toString() const { + std::string rv; + if (storage[0] == 1 << 7) { + // heap + rv.reserve(size()); + + uint32_t tableIndex = (storage[1] << 16) + (storage[2] << 8) + storage[3]; + uint16_t offset = (storage[4] << 8) + storage[5]; + + char* data = tables[tableIndex] + offset; + rv.append(data, size()); + return rv; + } + + for (int i = 0; i < storage[0]; i++) + rv += storage[i + 1]; + return rv; +} diff --git a/test/pooled_string.test.cpp b/test/pooled_string.test.cpp new file mode 100644 index 00000000..d32d1ccd --- /dev/null +++ b/test/pooled_string.test.cpp @@ -0,0 +1,34 @@ +#include +#include "external/minunit.h" +#include "pooled_string.h" + +MU_TEST(test_pooled_string) { + mu_check(PooledString("").size() == 0); + mu_check(PooledString("").toString() == ""); + mu_check(PooledString("f").size() == 1); + mu_check(PooledString("f").toString() == "f"); + mu_check(PooledString("hi").size() == 2); + mu_check(PooledString("f") == PooledString("f")); + mu_check(PooledString("f") != PooledString("g")); + + mu_check(PooledString("this is more than fifteen bytes").size() == 31); + mu_check(PooledString("this is more than fifteen bytes") != PooledString("f")); + + PooledString big("this is also a really long string"); + mu_check(big == big); + mu_check(big.toString() == "this is also a really long string"); + + PooledString big2("this is also a quite long string"); + mu_check(big != big2); + mu_check(big.toString() != big2.toString()); +} + +MU_TEST_SUITE(test_suite_pooled_string) { + MU_RUN_TEST(test_pooled_string); +} + +int main() { + MU_RUN_SUITE(test_suite_pooled_string); + MU_REPORT(); + return MU_EXIT_CODE; +} From 3eb07c2cb15236dae3d557f097a9743d1cc0c3e5 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 15:47:57 -0500 Subject: [PATCH 14/81] add tests for attribute store ...I'm going to replace the string implementation, so let's have some backstop to make sure I don't break things --- Makefile | 8 +++- include/attribute_store.h | 2 + src/attribute_store.cpp | 9 ++++- test/attribute_store.test.cpp | 70 +++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 test/attribute_store.test.cpp diff --git a/Makefile b/Makefile index 84fa084c..8bd26611 100644 --- a/Makefile +++ b/Makefile @@ -125,7 +125,13 @@ tilemaker: \ src/write_geometry.o $(CXX) $(CXXFLAGS) -o tilemaker $^ $(INC) $(LIB) $(LDFLAGS) -test: test_pooled_string test_sorted_way_store +test: test_attribute_store test_pooled_string test_sorted_way_store + +test_attribute_store: \ + src/mmap_allocator.o \ + src/attribute_store.o \ + test/attribute_store.test.o + $(CXX) $(CXXFLAGS) -o test.attribute_store $^ $(INC) $(LIB) $(LDFLAGS) && ./test.attribute_store test_pooled_string: \ src/mmap_allocator.o \ diff --git a/include/attribute_store.h b/include/attribute_store.h index 9d606139..1482db23 100644 --- a/include/attribute_store.h +++ b/include/attribute_store.h @@ -10,6 +10,7 @@ #include #include #include +#include "pooled_string.h" /* AttributeStore - global dictionary for attributes */ @@ -423,6 +424,7 @@ struct AttributeSet { struct AttributeStore { AttributeIndex add(AttributeSet &attributes); std::vector getUnsafe(AttributeIndex index) const; + size_t size() const; void reportSize() const; void finalize(); diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index 83acc1ac..46801668 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -307,11 +307,16 @@ std::vector AttributeStore::getUnsafe(AttributeIndex index } } -void AttributeStore::reportSize() const { +size_t AttributeStore::size() const { size_t numAttributeSets = 0; for (int i = 0; i < ATTRIBUTE_SHARDS; i++) numAttributeSets += sets[i].size(); - std::cout << "Attributes: " << numAttributeSets << " sets from " << lookups.load() << " objects" << std::endl; + + return numAttributeSets; +} + +void AttributeStore::reportSize() const { + std::cout << "Attributes: " << size() << " sets from " << lookups.load() << " objects" << std::endl; // Print detailed histogram of frequencies of attributes. if (false) { diff --git a/test/attribute_store.test.cpp b/test/attribute_store.test.cpp new file mode 100644 index 00000000..4fb1f979 --- /dev/null +++ b/test/attribute_store.test.cpp @@ -0,0 +1,70 @@ +#include +#include +#include "external/minunit.h" +#include "attribute_store.h" + +MU_TEST(test_attribute_store) { + AttributeStore store; + + mu_check(store.size() == 0); + + AttributeSet s1; + store.addAttribute(s1, "str1", std::string("someval"), 0); + store.addAttribute(s1, "str2", std::string("a very long string"), 0); + store.addAttribute(s1, "bool1", false, 0); + store.addAttribute(s1, "bool2", true, 0); + store.addAttribute(s1, "float1", (float)42.0, 0); + + const auto s1Index = store.add(s1); + + mu_check(store.size() == 1); + + const auto s1Pairs = store.getUnsafe(s1Index); + mu_check(s1Pairs.size() == 5); + + const auto str1 = std::find_if(s1Pairs.begin(), s1Pairs.end(), [&store](auto ap) { + return ap->keyIndex == store.keyStore.key2index("str1"); + }); + mu_check(str1 != s1Pairs.end()); + mu_check((*str1)->hasStringValue()); + mu_check((*str1)->stringValue() == "someval"); + + const auto str2 = std::find_if(s1Pairs.begin(), s1Pairs.end(), [&store](auto ap) { + return ap->keyIndex == store.keyStore.key2index("str2"); + }); + mu_check(str2 != s1Pairs.end()); + mu_check((*str2)->hasStringValue()); + mu_check((*str2)->stringValue() == "a very long string"); + + const auto bool1 = std::find_if(s1Pairs.begin(), s1Pairs.end(), [&store](auto ap) { + return ap->keyIndex == store.keyStore.key2index("bool1"); + }); + mu_check(bool1 != s1Pairs.end()); + mu_check((*bool1)->hasBoolValue()); + mu_check((*bool1)->boolValue() == false); + + const auto bool2 = std::find_if(s1Pairs.begin(), s1Pairs.end(), [&store](auto ap) { + return ap->keyIndex == store.keyStore.key2index("bool2"); + }); + mu_check(bool2 != s1Pairs.end()); + mu_check((*bool2)->hasBoolValue()); + mu_check((*bool2)->boolValue() == true); + + const auto float1 = std::find_if(s1Pairs.begin(), s1Pairs.end(), [&store](auto ap) { + return ap->keyIndex == store.keyStore.key2index("float1"); + }); + mu_check(float1 != s1Pairs.end()); + mu_check((*float1)->hasFloatValue()); + mu_check((*float1)->floatValue() == 42); + +} + +MU_TEST_SUITE(test_suite_attribute_store) { + MU_RUN_TEST(test_attribute_store); +} + +int main() { + MU_RUN_SUITE(test_suite_attribute_store); + MU_REPORT(); + return MU_EXIT_CODE; +} From b3eac9958caee521d1f8d5275d214072e15696cc Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 16:37:05 -0500 Subject: [PATCH 15/81] rejig isHot Break dependency on AttributePair, just work on std::string --- include/attribute_store.h | 20 +++----------------- src/attribute_store.cpp | 6 +++--- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/include/attribute_store.h b/include/attribute_store.h index 1482db23..345fc81d 100644 --- a/include/attribute_store.h +++ b/include/attribute_store.h @@ -123,7 +123,7 @@ struct AttributePair { float floatValue() const { return floatValue_; } bool boolValue() const { return floatValue_; } - static bool isHot(const AttributePair& pair, const std::string& keyName) { + static bool isHot(const std::string& keyName, const std::string& value) { // Is this pair a candidate for the hot pool? // Hot pairs are pairs that we think are likely to be re-used, like @@ -132,25 +132,11 @@ struct AttributePair { // The trick is that we commit to putting them in the hot pool // before we know if we were right. - // All boolean pairs are eligible. - if (pair.hasBoolValue()) - return true; - - // Small integers are eligible. - if (pair.hasFloatValue()) { - float v = pair.floatValue(); - - if (ceil(v) == v && v >= 0 && v <= 25) - return true; - } - - // The remaining things should be strings, but just in case... - if (!pair.hasStringValue()) - return false; + // The rules for floats/booleans are managed in their addAttribute call. // Only strings that are IDish are eligible: only lowercase letters. bool ok = true; - for (const auto& c: pair.stringValue()) { + for (const auto& c: value) { if (c != '-' && c != '_' && (c < 'a' || c > 'z')) return false; } diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index 46801668..beb68cf7 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -200,19 +200,19 @@ void AttributeSet::removePairWithKey(const AttributePairStore& pairStore, uint32 void AttributeStore::addAttribute(AttributeSet& attributeSet, std::string const &key, const std::string& v, char minzoom) { AttributePair kv(keyStore.key2index(key),v,minzoom); - bool isHot = AttributePair::isHot(kv, key); + bool isHot = AttributePair::isHot(key, v); attributeSet.removePairWithKey(pairStore, kv.keyIndex); attributeSet.addPair(pairStore.addPair(kv, isHot)); } void AttributeStore::addAttribute(AttributeSet& attributeSet, std::string const &key, bool v, char minzoom) { AttributePair kv(keyStore.key2index(key),v,minzoom); - bool isHot = AttributePair::isHot(kv, key); + bool isHot = true; // All bools are eligible to be hot pairs attributeSet.removePairWithKey(pairStore, kv.keyIndex); attributeSet.addPair(pairStore.addPair(kv, isHot)); } void AttributeStore::addAttribute(AttributeSet& attributeSet, std::string const &key, float v, char minzoom) { AttributePair kv(keyStore.key2index(key),v,minzoom); - bool isHot = AttributePair::isHot(kv, key); + bool isHot = v >= 0 && v <= 25 && ceil(v) == v; // Whole numbers in 0..25 are eligible to be hot pairs attributeSet.removePairWithKey(pairStore, kv.keyIndex); attributeSet.addPair(pairStore.addPair(kv, isHot)); } From 2784903b3945969abb5e0cb470139f669a4d1171 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 16:37:53 -0500 Subject: [PATCH 16/81] teach PooledString to work with std::string ...this will be useful for doing map lookups when testing if an AttributePair has already been created with the given value. --- include/pooled_string.h | 36 ++++++++++++++------ src/pooled_string.cpp | 68 ++++++++++++++++++++++++++++++++----- test/pooled_string.test.cpp | 21 ++++++++++++ 3 files changed, 106 insertions(+), 19 deletions(-) diff --git a/include/pooled_string.h b/include/pooled_string.h index 2fbeca37..95edd454 100644 --- a/include/pooled_string.h +++ b/include/pooled_string.h @@ -1,20 +1,19 @@ #ifndef _POOLED_STRING_H #define _POOLED_STRING_H -// std::string allows the allocated size to differ from the used size, -// which means it needs an extra pointer. It also supports large strings. +// std::string is quite general: +// - mutable +// - unlimited length +// - capacity can differ from size +// - can deallocate its dynamic memory // -// Our use case does not require this: we have immutable strings and always -// know their exact size, which fit in 64K. +// Our use case, by contrast is immutable, bounded strings that live for the +// duration of the process. // -// Further, g++'s implementation of std::string is inefficient - it takes 32 -// bytes (vs clang's 24 bytes), while only allowing a small-string optimization -// for strings of length 15 or less. +// This gives us some room to have less memory overhead, especially on +// g++, whose implementation of std::string requires 32 bytes. // -// std::string also needs to be able to free its allocated memory -- in our case, -// we're fine with the memory living until the process dies. -// -// Instead, we implemented `PooledString`. It has a size of 16 bytes, and a small +// Thus, we implement `PooledString`. It has a size of 16 bytes, and a small // string optimization for strings <= 15 bytes. (We will separately teach // AttributePair to encode Latin-character strings more efficiently, so that many // strings of size 24 or less fit in 15 bytes.) @@ -22,17 +21,32 @@ // If it needs to allocate memory, it does so from a shared pool. It is unable // to free the memory once allocated. +// PooledString has one of three modes: +// - [126:127] = 00: small-string, length is in [120:125], lower 15 bytes are string +// - [126:127] = 10: pooled string, table is in bytes 1..3, offset in bytes 4..5, length in bytes 6..7 +// - [126:127] = 11: pointer to std::string, pointer is in bytes 8..15 +// +// Note that the pointer mode is not safe to be stored. It exists just to allow +// lookups in the AttributePair map before deciding to allocate a string. + #include #include namespace PooledStringNS { class PooledString { public: + // Create a short string or heap string, long-lived. PooledString(const std::string& str); + + + // Create a std string - only valid so long as the string that is + // pointed to is valid. + PooledString(const std::string* str); size_t size() const; bool operator==(const PooledString& other) const; bool operator!=(const PooledString& other) const; std::string toString() const; + const char* data() const; private: // 0..3 is index into table, 4..5 is offset, 6..7 is length diff --git a/src/pooled_string.cpp b/src/pooled_string.cpp index 031a5c06..cc4532f4 100644 --- a/src/pooled_string.cpp +++ b/src/pooled_string.cpp @@ -6,6 +6,10 @@ namespace PooledStringNS { std::vector tables; std::mutex mutex; + const uint8_t ShortString = 0b00; + const uint8_t HeapString = 0b10; + const uint8_t StdString = 0b11; + // Each thread has its own string table, we only take a lock // to push a new table onto the vector. thread_local int64_t tableIndex = -1; @@ -52,14 +56,33 @@ PooledString::PooledString(const std::string& str) { } } +PooledString::PooledString(const std::string* str) { + storage[0] = StdString << 6; + + *(const std::string**)((void*)(storage + 8)) = str; +} + bool PooledStringNS::PooledString::operator==(const PooledString& other) const { // NOTE: We have surprising equality semantics! // - // For short strings, you are equal if the strings are equal. + // If one of the strings is a StdString, it's value equality. + // + // Else, for short strings, you are equal if the strings are equal. // // For large strings, you are equal if you use the same heap memory locations. // This implies that someone outside of PooledString is managing pooling! In our // case, it is the responsibility of AttributePairStore. + uint8_t kind = storage[0] >> 6; + uint8_t otherKind = other.storage[0] >> 6; + + if (kind == StdString || otherKind == StdString) { + size_t mySize = size(); + if (mySize != other.size()) + return false; + + return memcmp(data(), other.data(), mySize) == 0; + } + return memcmp(storage, other.storage, 16) == 0; } @@ -67,20 +90,44 @@ bool PooledStringNS::PooledString::operator!=(const PooledString& other) const { return !(*this == other); } +const char* PooledStringNS::PooledString::data() const { + uint8_t kind = storage[0] >> 6; + + if (kind == ShortString) + return (char *)(storage + 1); + + if (kind == StdString) { + const std::string* str = *(const std::string**)((void*)(storage + 8)); + return str->data(); + } + + uint32_t tableIndex = (storage[1] << 16) + (storage[2] << 8) + storage[3]; + uint16_t offset = (storage[4] << 8) + storage[5]; + + const char* data = tables[tableIndex] + offset; + return data; +} + size_t PooledStringNS::PooledString::size() const { + uint8_t kind = storage[0] >> 6; // If the uppermost bit is set, we're in heap. - if (storage[0] >> 7) { + if (kind == HeapString) { uint16_t length = (storage[6] << 8) + storage[7]; return length; } - // Otherwise it's stored in the lower 7 bits of the highest byte. - return storage[0] & 0b01111111; + if (kind == ShortString) + // Otherwise it's stored in the lower 7 bits of the highest byte. + return storage[0] & 0b01111111; + + const std::string* str = *(const std::string**)((void*)(storage + 8)); + return str->size(); } std::string PooledStringNS::PooledString::toString() const { std::string rv; - if (storage[0] == 1 << 7) { + uint8_t kind = storage[0] >> 6; + if (kind == HeapString) { // heap rv.reserve(size()); @@ -92,7 +139,12 @@ std::string PooledStringNS::PooledString::toString() const { return rv; } - for (int i = 0; i < storage[0]; i++) - rv += storage[i + 1]; - return rv; + if (kind == ShortString) { + for (int i = 0; i < storage[0]; i++) + rv += storage[i + 1]; + return rv; + } + + const std::string* str = *(const std::string**)((void*)(storage + 8)); + return *str; } diff --git a/test/pooled_string.test.cpp b/test/pooled_string.test.cpp index d32d1ccd..91fb2da5 100644 --- a/test/pooled_string.test.cpp +++ b/test/pooled_string.test.cpp @@ -21,6 +21,27 @@ MU_TEST(test_pooled_string) { PooledString big2("this is also a quite long string"); mu_check(big != big2); mu_check(big.toString() != big2.toString()); + + std::string shortString("short"); + std::string longString("this is a very long string"); + + PooledString stdShortString(&shortString); + mu_check(stdShortString.size() == 5); + mu_check(stdShortString.toString() == "short"); + + PooledString stdLongString(&longString); + mu_check(stdLongString.size() == 26); + mu_check(stdLongString.toString() == "this is a very long string"); + + // PooledStrings that are backed by std::string have the usual + // == semantics. + mu_check(stdShortString == PooledString("short")); + mu_check(PooledString("short") == stdShortString); + + mu_check(stdLongString == PooledString("this is a very long string")); + mu_check(PooledString("this is a very long string") == stdLongString); + + mu_check(stdShortString != stdLongString); } MU_TEST_SUITE(test_suite_pooled_string) { From efe6af959884304fe6783360ed94c8e29346dd07 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 18:02:12 -0500 Subject: [PATCH 17/81] use PooledString in AttributePair AttributePair has now been trimmed from 48 bytes to 18 bytes. There are 40M AttributeSets for the planet. That suggests there's probably ~30M AttributePairs, so hopefully this is a savings of ~900MB at the planet level. Runtime doesn't seem affected. There's a further opportunity for savings if we can make more strings qualify for the short string optimization. Only about 40% of strings fit in the 15 byte short string optimization. Of the remaining 60%, many are Latin-alphabet title cased strings like `Wellington Avenue` -- this could be encoded using 5 bits per letter, saving us an allocation. Even in the most optimistic case where: - there are 30M AttributePairs - of these, 90% are strings (= 27M) - of these, 60% don't fit in SSO (=16m) - of these, we can make 100% fit in SSO ...we only save about 256MB at the planet level, but at some significant complexity cost. So probably not worth pursuing at the moment. --- Makefile | 1 + include/attribute_store.h | 35 +++++++++++++---------------------- include/pooled_string.h | 2 ++ src/attribute_store.cpp | 26 ++++++++++++++++++++++++-- src/output_object.cpp | 9 ++++++--- src/pooled_string.cpp | 20 ++++++++++++++++++++ src/tilemaker.cpp | 1 - test/attribute_store.test.cpp | 34 ++++++++++++++++++++++++++++++++++ 8 files changed, 100 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 8bd26611..f8ff37c3 100644 --- a/Makefile +++ b/Makefile @@ -130,6 +130,7 @@ test: test_attribute_store test_pooled_string test_sorted_way_store test_attribute_store: \ src/mmap_allocator.o \ src/attribute_store.o \ + src/pooled_string.o \ test/attribute_store.test.o $(CXX) $(CXXFLAGS) -o test.attribute_store $^ $(INC) $(LIB) $(LDFLAGS) && ./test.attribute_store diff --git a/include/attribute_store.h b/include/attribute_store.h index 345fc81d..194f62f6 100644 --- a/include/attribute_store.h +++ b/include/attribute_store.h @@ -49,14 +49,14 @@ struct AttributePair { char minzoom : 4; union { float floatValue_; - std::string stringValue_; + PooledString stringValue_; }; AttributePair(uint32_t keyIndex, bool value, char minzoom) : keyIndex(keyIndex), valueType(AttributePairType::Bool), minzoom(minzoom), floatValue_(value ? 1 : 0) { } - AttributePair(uint32_t keyIndex, const std::string& value, char minzoom) + AttributePair(uint32_t keyIndex, const PooledString& value, char minzoom) : keyIndex(keyIndex), valueType(AttributePairType::String), stringValue_(value), minzoom(minzoom) { } @@ -65,13 +65,6 @@ struct AttributePair { { } - ~AttributePair() { - if (valueType == AttributePairType::Bool || valueType == AttributePairType::Float) - return; - - stringValue_.~basic_string(); - } - AttributePair(const AttributePair& other): keyIndex(other.keyIndex), valueType(other.valueType), minzoom(other.minzoom) { @@ -80,15 +73,10 @@ struct AttributePair { return; } - new (&stringValue_) std::string; stringValue_ = other.stringValue_; } AttributePair& operator=(const AttributePair& other) { - if (!(valueType == AttributePairType::Bool || valueType == AttributePairType::Float)) { - stringValue_.~basic_string(); - } - keyIndex = other.keyIndex; valueType = other.valueType; minzoom = other.minzoom; @@ -98,9 +86,7 @@ struct AttributePair { return *this; } - new (&stringValue_) std::string; stringValue_ = other.stringValue_; - return *this; } @@ -119,10 +105,13 @@ struct AttributePair { bool hasFloatValue() const { return valueType == AttributePairType::Float; } bool hasBoolValue() const { return valueType == AttributePairType::Bool; } - const std::string& stringValue() const { return stringValue_; } + const PooledString& pooledString() const { return stringValue_; } + const std::string stringValue() const { return stringValue_.toString(); } float floatValue() const { return floatValue_; } bool boolValue() const { return floatValue_; } + void ensureStringIsOwned(); + static bool isHot(const std::string& keyName, const std::string& value) { // Is this pair a candidate for the hot pool? @@ -153,9 +142,10 @@ struct AttributePair { boost::hash_combine(rv, keyIndex); boost::hash_combine(rv, valueType); - if(hasStringValue()) - boost::hash_combine(rv, stringValue()); - else if(hasFloatValue()) + if(hasStringValue()) { + const char* data = pooledString().data(); + boost::hash_range(rv, data, data + pooledString().size()); + } else if(hasFloatValue()) boost::hash_combine(rv, floatValue()); else if(hasBoolValue()) boost::hash_combine(rv, boolValue()); @@ -198,7 +188,7 @@ class AttributePairStore { void finalize() { finalized = true; } const AttributePair& getPair(uint32_t i) const; const AttributePair& getPairUnsafe(uint32_t i) const; - uint32_t addPair(const AttributePair& pair, bool isHot); + uint32_t addPair(AttributePair& pair, bool isHot); struct key_value_less_ptr { bool operator()(AttributePair const* lhs, AttributePair const* rhs) const { @@ -208,7 +198,7 @@ class AttributePairStore { return lhs->keyIndex < rhs->keyIndex; if (lhs->valueType != rhs->valueType) return lhs->valueType < rhs->valueType; - if (lhs->hasStringValue()) return lhs->stringValue() < rhs->stringValue(); + if (lhs->hasStringValue()) return lhs->pooledString() < rhs->pooledString(); if (lhs->hasBoolValue()) return lhs->boolValue() < rhs->boolValue(); if (lhs->hasFloatValue()) return lhs->floatValue() < rhs->floatValue(); throw std::runtime_error("Invalid type in attribute store"); @@ -410,6 +400,7 @@ struct AttributeSet { struct AttributeStore { AttributeIndex add(AttributeSet &attributes); std::vector getUnsafe(AttributeIndex index) const; + void reset(); // used for testing size_t size() const; void reportSize() const; void finalize(); diff --git a/include/pooled_string.h b/include/pooled_string.h index 95edd454..56d44453 100644 --- a/include/pooled_string.h +++ b/include/pooled_string.h @@ -43,10 +43,12 @@ namespace PooledStringNS { // pointed to is valid. PooledString(const std::string* str); size_t size() const; + bool operator<(const PooledString& other) const; bool operator==(const PooledString& other) const; bool operator!=(const PooledString& other) const; std::string toString() const; const char* data() const; + void ensureStringIsOwned(); private: // 0..3 is index into table, 4..5 is offset, 6..7 is length diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index beb68cf7..71c0925b 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -55,6 +55,16 @@ const std::string& AttributeKeyStore::getKeyUnsafe(uint16_t index) const { return keys[index]; } +// AttributePair +void AttributePair::ensureStringIsOwned() { + // Before we store an AttributePair in our long-term storage, we need + // to make sure it's not pointing to a non-long-lived std::string. + if (valueType == AttributePairType::Bool || valueType == AttributePairType::Float) + return; + + stringValue_.ensureStringIsOwned(); +} + // AttributePairStore thread_local boost::container::flat_map tlsHotShardMap; thread_local uint16_t tlsHotShardSize = 0; @@ -68,6 +78,7 @@ const AttributePair& AttributePairStore::getPair(uint32_t i) const { std::lock_guard lock(pairsMutex[shard]); return pairs[shard].at(offset); }; + const AttributePair& AttributePairStore::getPairUnsafe(uint32_t i) const { // NB: This is unsafe if called before the PBF has been fully read. // If called during the output phase, it's safe. @@ -81,7 +92,7 @@ const AttributePair& AttributePairStore::getPairUnsafe(uint32_t i) const { return pairs[shard].at(offset); }; -uint32_t AttributePairStore::addPair(const AttributePair& pair, bool isHot) { +uint32_t AttributePairStore::addPair(AttributePair& pair, bool isHot) { if (isHot) { { // First, check our thread-local map. @@ -109,6 +120,7 @@ uint32_t AttributePairStore::addPair(const AttributePair& pair, bool isHot) { hotShardSize++; uint32_t offset = hotShardSize.load(); + pair.ensureStringIsOwned(); hotShard[offset] = pair; const AttributePair* ptr = &hotShard[offset]; uint32_t rv = (0 << (32 - SHARD_BITS)) + offset; @@ -138,6 +150,7 @@ uint32_t AttributePairStore::addPair(const AttributePair& pair, bool isHot) { if (offset >= (1 << (32 - SHARD_BITS))) throw std::out_of_range("pair shard overflow"); + pair.ensureStringIsOwned(); pairs[shard].push_back(pair); const AttributePair* ptr = &pairs[shard][offset]; uint32_t rv = (shard << (32 - SHARD_BITS)) + offset; @@ -199,7 +212,8 @@ void AttributeSet::removePairWithKey(const AttributePairStore& pairStore, uint32 } void AttributeStore::addAttribute(AttributeSet& attributeSet, std::string const &key, const std::string& v, char minzoom) { - AttributePair kv(keyStore.key2index(key),v,minzoom); + PooledString ps(&v); + AttributePair kv(keyStore.key2index(key), ps, minzoom); bool isHot = AttributePair::isHot(key, v); attributeSet.removePairWithKey(pairStore, kv.keyIndex); attributeSet.addPair(pairStore.addPair(kv, isHot)); @@ -373,6 +387,14 @@ void AttributeStore::reportSize() const { } } +void AttributeStore::reset() { + // This is only used for tests. + tlsKeys2Index.clear(); + tlsKeys2IndexSize = 0; + tlsHotShardMap.clear(); + tlsHotShardSize = 0; +} + void AttributeStore::finalize() { finalized = true; keyStore.finalize(); diff --git a/src/output_object.cpp b/src/output_object.cpp index b68fb27f..7f9f0edb 100644 --- a/src/output_object.cpp +++ b/src/output_object.cpp @@ -87,9 +87,12 @@ void OutputObject::writeAttributes( int OutputObject::findValue(const vector* valueList, const AttributePair& value) const { for (size_t i=0; isize(); i++) { const vector_tile::Tile_Value& v = valueList->at(i); - if (v.has_string_value() && value.hasStringValue() && v.string_value()==value.stringValue()) { return i; } - if (v.has_float_value() && value.hasFloatValue() && v.float_value() ==value.floatValue() ) { return i; } - if (v.has_bool_value() && value.hasBoolValue() && v.bool_value() ==value.boolValue() ) { return i; } + if (v.has_string_value() && value.hasStringValue()) { + const size_t valueSize = value.pooledString().size(); + if (valueSize == v.string_value().size() && memcmp(v.string_value().data(), value.pooledString().data(), valueSize) == 0) + return i; + } else if (v.has_float_value() && value.hasFloatValue() && v.float_value() ==value.floatValue() ) { return i; } + else if (v.has_bool_value() && value.hasBoolValue() && v.bool_value() ==value.boolValue() ) { return i; } } return -1; } diff --git a/src/pooled_string.cpp b/src/pooled_string.cpp index cc4532f4..500408d4 100644 --- a/src/pooled_string.cpp +++ b/src/pooled_string.cpp @@ -148,3 +148,23 @@ std::string PooledStringNS::PooledString::toString() const { const std::string* str = *(const std::string**)((void*)(storage + 8)); return *str; } + +void PooledStringNS::PooledString::ensureStringIsOwned() { + uint8_t kind = storage[0] >> 6; + + if (kind != StdString) + return; + + *this = PooledString(toString()); +} + +bool PooledStringNS::PooledString::operator<(const PooledString& other) const { + size_t mySize = size(); + size_t otherSize = other.size(); + + if (mySize != otherSize) + return mySize < otherSize; + + return memcmp(data(), other.data(), mySize) < 0; +} + diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index 68540384..cdc01975 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -163,7 +163,6 @@ vector parseBox(const string& bbox) { * Worker threads write the output tiles, and start in the outputProc function. */ int main(int argc, char* argv[]) { - // ---- Read command-line options vector inputFiles; string luaFile; diff --git a/test/attribute_store.test.cpp b/test/attribute_store.test.cpp index 4fb1f979..3f2e28e5 100644 --- a/test/attribute_store.test.cpp +++ b/test/attribute_store.test.cpp @@ -5,6 +5,7 @@ MU_TEST(test_attribute_store) { AttributeStore store; + store.reset(); mu_check(store.size() == 0); @@ -56,11 +57,44 @@ MU_TEST(test_attribute_store) { mu_check(float1 != s1Pairs.end()); mu_check((*float1)->hasFloatValue()); mu_check((*float1)->floatValue() == 42); +} + +MU_TEST(test_attribute_store_reuses) { + AttributeStore store; + store.reset(); + + mu_check(store.size() == 0); + + { + AttributeSet s1a; + store.addAttribute(s1a, "str1", std::string("someval"), 0); + const auto s1aIndex = store.add(s1a); + + AttributeSet s1b; + store.addAttribute(s1b, "str1", std::string("someval"), 0); + const auto s1bIndex = store.add(s1b); + + mu_check(s1aIndex == s1bIndex); + } + + { + AttributeSet s1a; + store.addAttribute(s1a, "str1", std::string("this is a very long string"), 0); + const auto s1aIndex = store.add(s1a); + + AttributeSet s1b; + store.addAttribute(s1b, "str1", std::string("this is a very long string"), 0); + const auto s1bIndex = store.add(s1b); + + mu_check(s1aIndex == s1bIndex); + } + } MU_TEST_SUITE(test_suite_attribute_store) { MU_RUN_TEST(test_attribute_store); + MU_RUN_TEST(test_attribute_store_reuses); } int main() { From 9394bc75c845ae1f475184162609c0a62fa920db Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 16 Dec 2023 22:15:22 -0500 Subject: [PATCH 18/81] log timings When doing the planet, especially on a box with limited memory, there are long periods with no output. Show some output so the user doesn't think things are hung. This also might be useful in detecting perf regressions more granularly. --- include/osm_mem_tiles.h | 2 ++ include/shp_mem_tiles.h | 2 ++ include/tile_data.h | 21 +++++++++++++++++++++ src/read_pbf.cpp | 16 +++++++++++++--- src/tile_data.cpp | 4 ++-- src/tilemaker.cpp | 24 ++++++++++++++++++++++-- 6 files changed, 62 insertions(+), 7 deletions(-) diff --git a/include/osm_mem_tiles.h b/include/osm_mem_tiles.h index 74aeb18f..e7aff7ee 100644 --- a/include/osm_mem_tiles.h +++ b/include/osm_mem_tiles.h @@ -37,6 +37,8 @@ class OsmMemTiles : public TileDataSource { const WayStore& wayStore ); + std::string name() const override { return "osm"; } + Geometry buildWayGeometry( const OutputGeometryType geomType, const NodeID objectID, diff --git a/include/shp_mem_tiles.h b/include/shp_mem_tiles.h index 267a0090..508921ff 100644 --- a/include/shp_mem_tiles.h +++ b/include/shp_mem_tiles.h @@ -11,6 +11,8 @@ class ShpMemTiles : public TileDataSource public: ShpMemTiles(size_t threadNum, uint baseZoom); + std::string name() const override { return "shp"; } + void CreateNamedLayerIndex(const std::string& layerName); // Used in shape file loading diff --git a/include/tile_data.h b/include/tile_data.h index e02a0255..f40c754c 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -46,13 +46,31 @@ struct OutputObjectXYID { }; template void finalizeObjects( + const std::string& name, const size_t& threadNum, const unsigned int& baseZoom, typename std::vector>>::iterator begin, typename std::vector>>::iterator end, typename std::vector>>& lowZoom ) { +#ifdef CLOCK_MONOTONIC + timespec startTs, endTs; + clock_gettime(CLOCK_MONOTONIC, &startTs); +#endif + + int i = 0; for (auto it = begin; it != end; it++) { + i++; + if (i % 10 == 0 || i == 4096) { + std::cout << "\r" << name << ": finalizing z6 tile " << i << "/" << CLUSTER_ZOOM_AREA; + +#ifdef CLOCK_MONOTONIC + clock_gettime(CLOCK_MONOTONIC, &endTs); + uint64_t elapsedNs = 1e9 * (endTs.tv_sec - startTs.tv_sec) + endTs.tv_nsec - startTs.tv_nsec; + std::cout << " (" << std::to_string((uint32_t)(elapsedNs / 1e6)) << " ms)"; +#endif + std::cout << std::flush; + } if (it->size() == 0) continue; @@ -109,6 +127,8 @@ template void finalizeObjects( threadNum ); } + + std::cout << std::endl; } template void collectTilesWithObjectsAtZoomTemplate( @@ -280,6 +300,7 @@ class TileDataSource { std::vector> availableMultiLinestringStoreLeases; std::vector> availableMultiPolygonStoreLeases; + virtual std::string name() const = 0; protected: size_t numShards; diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 605618fa..2eb1795c 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -273,7 +273,7 @@ bool PbfReader::ReadBlock( if (ioMutex.try_lock()) { std::ostringstream str; void_mmap_allocator::reportStoreSize(str); - str << "Block " << blocksProcessed.load() << "/" << blocksToProcess.load() << " ways " << pg.ways_size() << " relations " << pg.relations_size() << " \r"; + str << "\rBlock " << blocksProcessed.load() << "/" << blocksToProcess.load() << " ways " << pg.ways_size() << " relations " << pg.relations_size() << " "; std::cout << str.str(); std::cout.flush(); ioMutex.unlock(); @@ -293,7 +293,7 @@ bool PbfReader::ReadBlock( osmStore.ensureUsedWaysInited(); bool done = ScanRelations(output, pg, pb); if(done) { - std::cout << "(Scanning for ways used in relations: " << (100*blocksProcessed.load()/blocksToProcess.load()) << "%)\r"; + std::cout << "\r(Scanning for ways used in relations: " << (100*blocksProcessed.load()/blocksToProcess.load()) << "%) "; std::cout.flush(); continue; } @@ -459,6 +459,11 @@ int PbfReader::ReadPbfFile( std::vector all_phases = { ReadPhase::Nodes, ReadPhase::RelationScan, ReadPhase::Ways, ReadPhase::Relations }; for(auto phase: all_phases) { +#ifdef CLOCK_MONOTONIC + timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); +#endif + // Launch the pool with threadNum threads boost::asio::thread_pool pool(threadNum); std::mutex block_mutex; @@ -529,8 +534,8 @@ int PbfReader::ReadPbfFile( if(ReadBlock(*infile, *output, indexedBlockMetadata, nodeKeys, locationsOnWays, phase)) { const std::lock_guard lock(block_mutex); blocks.erase(indexedBlockMetadata.index); - blocksProcessed++; } + blocksProcessed++; } }); } @@ -538,6 +543,11 @@ int PbfReader::ReadPbfFile( pool.join(); +#ifdef CLOCK_MONOTONIC + clock_gettime(CLOCK_MONOTONIC, &end); + uint64_t elapsedNs = 1e9 * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; + std::cout << "(" << std::to_string((uint32_t)(elapsedNs / 1e6)) << " ms)" << std::endl; +#endif if(phase == ReadPhase::Nodes) { osmStore.nodes.finalize(threadNum); } diff --git a/src/tile_data.cpp b/src/tile_data.cpp index a50ddfea..d3fc15c2 100644 --- a/src/tile_data.cpp +++ b/src/tile_data.cpp @@ -74,8 +74,8 @@ TileDataSource::TileDataSource(size_t threadNum, unsigned int baseZoom, bool inc } void TileDataSource::finalize(size_t threadNum) { - finalizeObjects(threadNum, baseZoom, objects.begin(), objects.end(), lowZoomObjects); - finalizeObjects(threadNum, baseZoom, objectsWithIds.begin(), objectsWithIds.end(), lowZoomObjectsWithIds); + finalizeObjects(name(), threadNum, baseZoom, objects.begin(), objects.end(), lowZoomObjects); + finalizeObjects(name(), threadNum, baseZoom, objectsWithIds.begin(), objectsWithIds.end(), lowZoomObjectsWithIds); } diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index cdc01975..8a3f6419 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -506,8 +506,16 @@ int main(int argc, char* argv[]) { } std::deque> tileCoordinates; + std::cout << "collecting tiles:"; for (uint zoom=sharedData.config.startZoom; zoom <= sharedData.config.endZoom; zoom++) { + std::cout << " z" << std::to_string(zoom) << std::flush; +#ifdef CLOCK_MONOTONIC + timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); +#endif + auto zoomResult = getTilesAtZoom(sources, zoom); + int numTiles = 0; for (int x = 0; x < 1 << zoom; x++) { for (int y = 0; y < 1 << zoom; y++) { if (!zoomResult.test(x, y)) @@ -533,10 +541,22 @@ int main(int argc, char* argv[]) { } tileCoordinates.push_back(std::make_pair(zoom, TileCoordinates(x, y))); + numTiles++; } } + + std::cout << " (" << numTiles; +#ifdef CLOCK_MONOTONIC + clock_gettime(CLOCK_MONOTONIC, &end); + uint64_t tileNs = 1e9 * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; + std::cout << ", " << (uint32_t)(tileNs / 1e6) << "ms"; + +#endif + std::cout << ")" << std::flush; } + std::cout << std::endl; + // Cluster tiles: breadth-first for z0..z5, depth-first for z6 const size_t baseZoom = config.baseZoom; boost::sort::block_indirect_sort( @@ -615,7 +635,7 @@ int main(int argc, char* argv[]) { unsigned int zoom = tileCoordinates[i].first; TileCoordinates coords = tileCoordinates[i].second; -#ifndef _WIN32 +#ifdef CLOCK_MONOTONIC timespec start, end; if (logTileTimings) clock_gettime(CLOCK_MONOTONIC, &start); @@ -627,7 +647,7 @@ int main(int argc, char* argv[]) { } outputProc(sharedData, sources, attributeStore, data, coords, zoom); -#ifndef _WIN32 +#ifdef CLOCK_MONOTONIC if (logTileTimings) { clock_gettime(CLOCK_MONOTONIC, &end); uint64_t tileNs = 1e9 * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; From 3020011054c4813d84918b03493dd902e7961bd5 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 00:37:04 -0500 Subject: [PATCH 19/81] AppendVector: an append-only chunked vector When using --store, deque is nice because growing doesn't require invalidating the old storage and copying it to a new location. However, it's also bad, because deque allocates in 512-byte chunks, which causes each 4KB OS page to have data from different z6 tiles. Instead, use our own container that tries to get the best of both worlds. Writing a random access iterator is new for me, so I don't trust this code that much. The saving grace is that the container is very limited, so errors in the iterator impelementation may not get exercised in practice. --- Makefile | 7 +- include/append_vector.h | 193 ++++++++++++++++++++++++++++++++++++ include/tile_data.h | 21 ++-- test/append_vector.test.cpp | 85 ++++++++++++++++ 4 files changed, 294 insertions(+), 12 deletions(-) create mode 100644 include/append_vector.h create mode 100644 test/append_vector.test.cpp diff --git a/Makefile b/Makefile index f8ff37c3..1bddb089 100644 --- a/Makefile +++ b/Makefile @@ -125,7 +125,12 @@ tilemaker: \ src/write_geometry.o $(CXX) $(CXXFLAGS) -o tilemaker $^ $(INC) $(LIB) $(LDFLAGS) -test: test_attribute_store test_pooled_string test_sorted_way_store +test: test_append_vector test_attribute_store test_pooled_string test_sorted_way_store + +test_append_vector: \ + src/mmap_allocator.o \ + test/append_vector.test.o + $(CXX) $(CXXFLAGS) -o test.append_vector $^ $(INC) $(LIB) $(LDFLAGS) && ./test.append_vector test_attribute_store: \ src/mmap_allocator.o \ diff --git a/include/append_vector.h b/include/append_vector.h new file mode 100644 index 00000000..3fe9b907 --- /dev/null +++ b/include/append_vector.h @@ -0,0 +1,193 @@ +#ifndef _APPEND_VECTOR_H +#define _APPEND_VECTOR_H + +#include "mmap_allocator.h" +#include +#include + +// Tilemaker collects OutputObjects in a list that +// - spills to disk +// - only gets appended to +// +// Vector is great for linear access, but resizes cause expensive disk I/O to +// copy elements. +// +// Deque is great for growing without disk I/O, but it allocates in blocks of 512, +// which is inefficient for linear access. +// +// Instead, we author a limited vector-of-vectors class that allocates in bigger chunks, +// to get the best of both worlds. + +#define APPEND_VECTOR_SIZE 8192 +namespace AppendVectorNS { + template + class AppendVector { + public: + struct Iterator { + using iterator_category = std::random_access_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = T; + using pointer = T*; + using reference = T&; + + Iterator(AppendVector& appendVector, uint16_t vec, uint16_t offset): + appendVector(&appendVector), vec(vec), offset(offset) {} + + Iterator(): + appendVector(nullptr), vec(0), offset(0) {} + + + bool operator<(const Iterator& other) const { + if (vec < other.vec) + return true; + + if (vec > other.vec) + return false; + + return offset < other.offset; + } + + bool operator>=(const Iterator& other) const { + return !(*this < other); + } + + Iterator operator-(int delta) const { + int64_t absolute = vec * APPEND_VECTOR_SIZE + offset; + absolute -= delta; + return Iterator(*appendVector, absolute / APPEND_VECTOR_SIZE, absolute % APPEND_VECTOR_SIZE); + } + + Iterator operator+(int delta) const { + int64_t absolute = vec * APPEND_VECTOR_SIZE + offset; + absolute += delta; + return Iterator(*appendVector, absolute / APPEND_VECTOR_SIZE, absolute % APPEND_VECTOR_SIZE); + } + + bool operator==(const Iterator& other) const { + return appendVector == other.appendVector && vec == other.vec && offset == other.offset; + } + + bool operator!=(const Iterator& other) const { + return !(*this == other); + } + + std::ptrdiff_t operator-(const Iterator& other) const { + int64_t absolute = vec * APPEND_VECTOR_SIZE + offset; + int64_t otherAbsolute = other.vec * APPEND_VECTOR_SIZE + other.offset; + + return absolute - otherAbsolute; + } + + reference operator*() const { + auto& vector = appendVector->vecs[vec]; + auto& el = vector[offset]; + return el; + } + + pointer operator->() const { + auto& vector = appendVector->vecs[vec]; + auto& el = vector[offset]; + return ⪙ + } + + Iterator& operator+= (int delta) { + int64_t absolute = vec * APPEND_VECTOR_SIZE + offset; + absolute += delta; + + vec = absolute / APPEND_VECTOR_SIZE; + offset = absolute % APPEND_VECTOR_SIZE; + return *this; + } + + Iterator& operator-= (int delta) { + int64_t absolute = vec * APPEND_VECTOR_SIZE + offset; + absolute -= delta; + + vec = absolute / APPEND_VECTOR_SIZE; + offset = absolute % APPEND_VECTOR_SIZE; + return *this; + } + + // Prefix increment + Iterator& operator++() { + offset++; + if (offset == APPEND_VECTOR_SIZE) { + offset = 0; + vec++; + } + return *this; + } + + // Postfix increment + Iterator operator++(int) { Iterator tmp = *this; ++(*this); return tmp; } + + // Prefix decrement + Iterator& operator--() { + if (offset > 0) { + offset--; + } else { + vec--; + offset = APPEND_VECTOR_SIZE - 1; + } + + return *this; + } + + // Postfix decrement + Iterator operator--(int) { Iterator tmp = *this; --(*this); return tmp; } + + private: + mutable AppendVector* appendVector; + int32_t vec, offset; + }; + + AppendVector(): + count(0), + vecs(1) { + } + + void clear() { + count = 0; + vecs.clear(); + vecs.push_back(std::vector>()); + vecs.back().reserve(APPEND_VECTOR_SIZE); + } + + size_t size() const { + return count; + } + + T& operator [](int idx) { + return vecs[idx / APPEND_VECTOR_SIZE][idx % APPEND_VECTOR_SIZE]; + } + + Iterator begin() { + return Iterator(*this, 0, 0); + } + + Iterator end() { + return Iterator(*this, vecs.size() - 1, count % APPEND_VECTOR_SIZE); + } + + void push_back(const T& el) { + if (vecs.back().capacity() == 0) + vecs.back().reserve(APPEND_VECTOR_SIZE); + + vecs.back().push_back(el); + + if (vecs.back().size() == vecs.back().capacity()) { + vecs.push_back(std::vector>()); + vecs.back().reserve(APPEND_VECTOR_SIZE); + } + + count++; + } + + size_t count; + std::deque>> vecs; + }; +} + +#undef APPEND_VECTOR_SIZE + +#endif diff --git a/include/tile_data.h b/include/tile_data.h index f40c754c..78793c27 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -8,6 +8,7 @@ #include #include #include "output_object.h" +#include "append_vector.h" #include "clip_cache.h" #include "mmap_allocator.h" @@ -49,9 +50,9 @@ template void finalizeObjects( const std::string& name, const size_t& threadNum, const unsigned int& baseZoom, - typename std::vector>>::iterator begin, - typename std::vector>>::iterator end, - typename std::vector>>& lowZoom + typename std::vector>::iterator begin, + typename std::vector>::iterator end, + typename std::vector>& lowZoom ) { #ifdef CLOCK_MONOTONIC timespec startTs, endTs; @@ -74,8 +75,6 @@ template void finalizeObjects( if (it->size() == 0) continue; - it->shrink_to_fit(); - for (auto objectIt = it->begin(); objectIt != it->end(); objectIt++) if (objectIt->oo.minZoom < CLUSTER_ZOOM) lowZoom[0].push_back(*objectIt); @@ -133,7 +132,7 @@ template void finalizeObjects( template void collectTilesWithObjectsAtZoomTemplate( const unsigned int& baseZoom, - const typename std::vector>>::iterator objects, + const typename std::vector>::iterator objects, const size_t size, const unsigned int zoom, TileCoordinatesSet& output @@ -175,7 +174,7 @@ inline OutputObjectID outputObjectWithId(const OutputObjectXYI template void collectObjectsForTileTemplate( const unsigned int& baseZoom, - typename std::vector>>::iterator objects, + typename std::vector>::iterator objects, size_t iStart, size_t iEnd, unsigned int zoom, @@ -318,10 +317,10 @@ class TileDataSource { // // If config.include_ids is true, objectsWithIds will be populated. // Otherwise, objects. - std::vector>> objects; - std::vector>> lowZoomObjects; - std::vector>> objectsWithIds; - std::vector>> lowZoomObjectsWithIds; + std::vector> objects; + std::vector> lowZoomObjects; + std::vector> objectsWithIds; + std::vector> lowZoomObjectsWithIds; // rtree index of large objects using oo_rtree_param_type = boost::geometry::index::quadratic<128>; diff --git a/test/append_vector.test.cpp b/test/append_vector.test.cpp new file mode 100644 index 00000000..300f6e30 --- /dev/null +++ b/test/append_vector.test.cpp @@ -0,0 +1,85 @@ +#include +#include +#include "external/minunit.h" +#include "append_vector.h" + +using namespace AppendVectorNS; + +MU_TEST(test_append_vector) { + AppendVector vec; + mu_check(vec.size() == 0); + + for (int i = 0; i < 10000; i++) { + vec.push_back(i); + } + mu_check(vec.size() == 10000); + + mu_check(vec[25] == 25); + + const AppendVector::Iterator& it = vec.begin(); + mu_check(*it == 0); + mu_check(*(it + 1) == 1); + mu_check(*(it + 2) == 2); + mu_check(*(it + 9000) == 9000); + mu_check(*(it + 1 - 1) == 0); + mu_check(*(vec.end() + -1) == 9999); + mu_check(*(vec.end() - 1) == 9999); + mu_check(*(vec.end() - 2) == 9998); + mu_check(*(vec.end() - 9000) == 1000); + mu_check(*(vec.begin() - -1) == 1); + + boost::sort::block_indirect_sort( + vec.begin(), + vec.end(), + [](auto const &a, auto const&b) { return b < a; }, + 1 + ); + + mu_check(vec[0] == 9999); + mu_check(vec[9999] == 0); + + boost::sort::block_indirect_sort( + vec.begin(), + vec.end(), + [](auto const &a, auto const&b) { return a < b; }, + 1 + ); + + mu_check(vec[0] == 0); + mu_check(vec[9999] == 9999); + + auto iter = std::lower_bound( + vec.begin(), + vec.end(), + 123, + [](const uint32_t& a, const uint32_t& toFind) { + return a < toFind; + } + ); + + mu_check(iter != vec.end()); + mu_check(*iter == 123); + + iter = std::lower_bound( + vec.begin(), + vec.end(), + 123123, + [](const uint32_t& a, const uint32_t& toFind) { + return a < toFind; + } + ); + + mu_check(iter == vec.end()); + +} + +MU_TEST_SUITE(test_suite_append_vector) { + MU_RUN_TEST(test_append_vector); +} + +int main() { + MU_RUN_SUITE(test_suite_append_vector); + MU_REPORT(); + return MU_EXIT_CODE; +} + From 330b0a79a7e815df9af4bc0050b24a9d0f699647 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 01:22:47 -0500 Subject: [PATCH 20/81] fix progress when --store present --- src/read_pbf.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 2eb1795c..ff585d03 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -272,8 +272,9 @@ bool PbfReader::ReadBlock( { if (ioMutex.try_lock()) { std::ostringstream str; + str << "\r"; void_mmap_allocator::reportStoreSize(str); - str << "\rBlock " << blocksProcessed.load() << "/" << blocksToProcess.load() << " ways " << pg.ways_size() << " relations " << pg.relations_size() << " "; + str << "Block " << blocksProcessed.load() << "/" << blocksToProcess.load() << " ways " << pg.ways_size() << " relations " << pg.relations_size() << " "; std::cout << str.str(); std::cout.flush(); ioMutex.unlock(); From 9d97d30f8999c31b2af456b7ec7c21197eec606d Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 08:51:14 -0500 Subject: [PATCH 21/81] mutex on RelationScan progress output --- src/read_pbf.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index ff585d03..0202a67d 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -294,8 +294,11 @@ bool PbfReader::ReadBlock( osmStore.ensureUsedWaysInited(); bool done = ScanRelations(output, pg, pb); if(done) { - std::cout << "\r(Scanning for ways used in relations: " << (100*blocksProcessed.load()/blocksToProcess.load()) << "%) "; - std::cout.flush(); + if (ioMutex.try_lock()) { + std::cout << "\r(Scanning for ways used in relations: " << (100*blocksProcessed.load()/blocksToProcess.load()) << "%) "; + std::cout.flush(); + ioMutex.unlock(); + } continue; } } From f9993cf9534cd933903f4396ee8d0c91ffe3efd5 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 09:49:00 -0500 Subject: [PATCH 22/81] make NodeStore/WayStore shardable This adds three methods to the stores: - `shard()` returns which shard you are - `shards()` returns how many shards total - `contains(shard, id)` returns whether or not shard N has an item with id X SortedNodeStore/SortedWayStore are not implemented yet, that'll come in a future commit. This will allow us to create a `ShardedNodeStore` and `ShardedWayStore` that contain N stores. We will try to ensure that each store has data that is geographically close to each other. Then, when reading, we'll do multiple passes of the PBF to populate each store. This should let us reduce the working set used to populate the stores, at the cost of additional linear scans of the PBF. Linear scans of disk are much less painful than random scans, so that should be a good trade. --- include/node_store.h | 4 ++++ include/node_stores.h | 11 +++++++++++ include/sorted_node_store.h | 4 ++++ include/sorted_way_store.h | 4 ++++ include/way_store.h | 4 ++++ include/way_stores.h | 4 ++++ src/node_stores.cpp | 11 +++++++++++ src/way_stores.cpp | 8 ++++++++ test/sorted_way_store.test.cpp | 4 ++++ 9 files changed, 54 insertions(+) diff --git a/include/node_store.h b/include/node_store.h index cc84aba2..9ef2a4c6 100644 --- a/include/node_store.h +++ b/include/node_store.h @@ -23,6 +23,10 @@ class NodeStore // Accessors virtual size_t size() const = 0; virtual LatpLon at(NodeID i) const = 0; + + virtual bool contains(size_t shard, NodeID id) const = 0; + virtual size_t shard() const = 0; + virtual size_t shards() const = 0; }; #endif diff --git a/include/node_stores.h b/include/node_stores.h index c5151bec..f093081f 100644 --- a/include/node_stores.h +++ b/include/node_stores.h @@ -24,6 +24,11 @@ class BinarySearchNodeStore : public NodeStore } void batchStart() {} + bool contains(size_t shard, NodeID id) const override; + size_t shard() const override { return 0; } + size_t shards() const override { return 1; } + + private: mutable std::mutex mutex; std::vector> mLatpLons; @@ -51,6 +56,12 @@ class CompactNodeStore : public NodeStore void finalize(size_t numThreads) override {} void batchStart() {} + // CompactNodeStore has no metadata to know whether or not it contains + // a node, so it's not suitable for used in sharded scenarios. + bool contains(size_t shard, NodeID id) const override { return true; } + size_t shard() const override { return 0; } + size_t shards() const override { return 1; } + private: // @brief Insert a latp/lon pair. // @param i OSM ID of a node diff --git a/include/sorted_node_store.h b/include/sorted_node_store.h index 5c156ad3..20bad4e0 100644 --- a/include/sorted_node_store.h +++ b/include/sorted_node_store.h @@ -69,6 +69,10 @@ class SortedNodeStore : public NodeStore reopen(); } + bool contains(size_t shard, NodeID ID) const override { throw std::runtime_error("SortedNodeStore::contains not implemented"); } + size_t shard() const override { return 0; } + size_t shards() const override { return 1; } + private: // When true, store chunks compressed. Only store compressed if the // chunk is sufficiently large. diff --git a/include/sorted_way_store.h b/include/sorted_way_store.h index 145e467b..448fffda 100644 --- a/include/sorted_way_store.h +++ b/include/sorted_way_store.h @@ -93,6 +93,10 @@ class SortedWayStore: public WayStore { void clear() override; std::size_t size() const override; void finalize(unsigned int threadNum) override; + + bool contains(size_t shard, WayID id) const override { throw std::runtime_error("SortedWayStore::contains not implemented"); } + size_t shard() const override { return 0; } + size_t shards() const override { return 1; } static uint16_t encodeWay( const std::vector& way, diff --git a/include/way_store.h b/include/way_store.h index 8650cbea..5e274a5c 100644 --- a/include/way_store.h +++ b/include/way_store.h @@ -21,6 +21,10 @@ class WayStore { virtual void clear() = 0; virtual std::size_t size() const = 0; virtual void finalize(unsigned int threadNum) = 0; + + virtual bool contains(size_t shard, WayID id) const = 0; + virtual size_t shard() const = 0; + virtual size_t shards() const = 0; }; #endif diff --git a/include/way_stores.h b/include/way_stores.h index dfb5f74c..4ed8db7e 100644 --- a/include/way_stores.h +++ b/include/way_stores.h @@ -21,6 +21,10 @@ class BinarySearchWayStore: public WayStore { std::size_t size() const override; void finalize(unsigned int threadNum) override; + bool contains(size_t shard, WayID id) const override; + size_t shard() const override { return 0; } + size_t shards() const override { return 1; } + private: mutable std::mutex mutex; std::unique_ptr mLatpLonLists; diff --git a/src/node_stores.cpp b/src/node_stores.cpp index 8c84b811..06e2fc5e 100644 --- a/src/node_stores.cpp +++ b/src/node_stores.cpp @@ -14,6 +14,17 @@ void BinarySearchNodeStore::reopen() } } +bool BinarySearchNodeStore::contains(size_t shard, NodeID i) const { + auto internalShard = mLatpLons[shardPart(i)]; + auto id = idPart(i); + + auto iter = std::lower_bound(internalShard->begin(), internalShard->end(), id, [](auto const &e, auto i) { + return e.first < i; + }); + + return !(iter == internalShard->end() || iter->first != id); +} + LatpLon BinarySearchNodeStore::at(NodeID i) const { auto shard = mLatpLons[shardPart(i)]; auto id = idPart(i); diff --git a/src/way_stores.cpp b/src/way_stores.cpp index 05d884d0..e19cbf5a 100644 --- a/src/way_stores.cpp +++ b/src/way_stores.cpp @@ -14,6 +14,14 @@ void BinarySearchWayStore::reopen() { mLatpLonLists = std::make_unique(); } +bool BinarySearchWayStore::contains(size_t shard, WayID id) const { + auto iter = std::lower_bound(mLatpLonLists->begin(), mLatpLonLists->end(), id, [](auto const &e, auto id) { + return e.first < id; + }); + + return !(iter == mLatpLonLists->end() || iter->first != id); +} + std::vector BinarySearchWayStore::at(WayID wayid) const { std::lock_guard lock(mutex); diff --git a/test/sorted_way_store.test.cpp b/test/sorted_way_store.test.cpp index 1c50a494..217a1110 100644 --- a/test/sorted_way_store.test.cpp +++ b/test/sorted_way_store.test.cpp @@ -13,6 +13,10 @@ class TestNodeStore : public NodeStore { return { (int32_t)id, -(int32_t)id }; } void insert(const std::vector>& elements) override {} + + bool contains(size_t shard, NodeID id) const override { return true; } + size_t shard() const override { return 0; } + size_t shards() const override { return 1; } }; void roundtripWay(const std::vector& way) { From b49b1e7da4f8293518451e4a367e9cd846074177 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 10:08:02 -0500 Subject: [PATCH 23/81] add minimal SortedNodeStore test I'm going to rejig the innards of this class, so let's have some tests. --- Makefile | 17 ++++++++++++++++- test/sorted_node_store.test.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 test/sorted_node_store.test.cpp diff --git a/Makefile b/Makefile index 1bddb089..9eae44c3 100644 --- a/Makefile +++ b/Makefile @@ -125,7 +125,12 @@ tilemaker: \ src/write_geometry.o $(CXX) $(CXXFLAGS) -o tilemaker $^ $(INC) $(LIB) $(LDFLAGS) -test: test_append_vector test_attribute_store test_pooled_string test_sorted_way_store +test: \ + test_append_vector \ + test_attribute_store \ + test_pooled_string \ + test_sorted_node_store \ + test_sorted_way_store test_append_vector: \ src/mmap_allocator.o \ @@ -145,6 +150,16 @@ test_pooled_string: \ test/pooled_string.test.o $(CXX) $(CXXFLAGS) -o test.pooled_string $^ $(INC) $(LIB) $(LDFLAGS) && ./test.pooled_string +test_sorted_node_store: \ + src/external/streamvbyte_decode.o \ + src/external/streamvbyte_encode.o \ + src/external/streamvbyte_zigzag.o \ + src/mmap_allocator.o \ + src/sorted_node_store.o \ + test/sorted_node_store.test.o + $(CXX) $(CXXFLAGS) -o test.sorted_node_store $^ $(INC) $(LIB) $(LDFLAGS) && ./test.sorted_node_store + + test_sorted_way_store: \ src/external/streamvbyte_decode.o \ src/external/streamvbyte_encode.o \ diff --git a/test/sorted_node_store.test.cpp b/test/sorted_node_store.test.cpp new file mode 100644 index 00000000..ea6956d6 --- /dev/null +++ b/test/sorted_node_store.test.cpp @@ -0,0 +1,27 @@ +#include +#include "external/minunit.h" +#include "sorted_node_store.h" + +MU_TEST(test_sorted_node_store) { + SortedNodeStore sns(true); + mu_check(sns.size() == 0); + + sns.batchStart(); + + sns.insert({ {1, {2, 3 } } }); + + sns.finalize(1); + + mu_check(sns.size() == 1); + +} + +MU_TEST_SUITE(test_suite_sorted_node_store) { + MU_RUN_TEST(test_sorted_node_store); +} + +int main() { + MU_RUN_SUITE(test_suite_sorted_node_store); + MU_REPORT(); + return MU_EXIT_CODE; +} From e81c6ee0c5fcc6fe82de9c26f487dd9151624882 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 10:13:28 -0500 Subject: [PATCH 24/81] stop using internal linkage for atomics In order to shard the stores, we need to have multiple instances of the class. Two things block this currently: atomics at file-level, and thread-locals. Moving the atomics to the class is easy. Making the thread-locals per-class will require an approach similar to that adopted in https://github.com/systemed/tilemaker/blob/52b62dfbd5b6f8e4feb6cad4e3de86ba27874b3a/include/leased_store.h#L48, where we have a container that tracks the per-class data. --- include/sorted_node_store.h | 10 ++++++++++ include/sorted_way_store.h | 8 ++++++++ src/sorted_node_store.cpp | 20 ++++++-------------- src/sorted_way_store.cpp | 17 ++++++----------- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/include/sorted_node_store.h b/include/sorted_node_store.h index 20bad4e0..8f276f4a 100644 --- a/include/sorted_node_store.h +++ b/include/sorted_node_store.h @@ -3,6 +3,7 @@ #include "node_store.h" #include "mmap_allocator.h" +#include #include #include #include @@ -86,6 +87,15 @@ class SortedNodeStore : public NodeStore // multiple threads. They'll get folded into the index during finalize() std::map> orphanage; std::vector> workerBuffers; + + std::atomic totalGroups; + std::atomic totalNodes; + std::atomic totalGroupSpace; + std::atomic totalAllocatedSpace; + std::atomic totalChunks; + std::atomic chunkSizeFreqs[257]; + std::atomic groupSizeFreqs[257]; + void collectOrphans(const std::vector& orphans); void publishGroup(const std::vector& nodes); }; diff --git a/include/sorted_way_store.h b/include/sorted_way_store.h index 448fffda..b28c4257 100644 --- a/include/sorted_way_store.h +++ b/include/sorted_way_store.h @@ -1,6 +1,7 @@ #ifndef _SORTED_WAY_STORE_H #define _SORTED_WAY_STORE_H +#include #include #include #include @@ -117,6 +118,13 @@ class SortedWayStore: public WayStore { // multiple threads. They'll get folded into the index during finalize() std::map>>> orphanage; std::vector>>> workerBuffers; + + std::atomic totalWays; + std::atomic totalNodes; + std::atomic totalGroups; + std::atomic totalGroupSpace; + std::atomic totalChunks; + void collectOrphans(const std::vector>>& orphans); void publishGroup(const std::vector>>& ways); }; diff --git a/src/sorted_node_store.cpp b/src/sorted_node_store.cpp index 76aa81b8..0f856af6 100644 --- a/src/sorted_node_store.cpp +++ b/src/sorted_node_store.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include "sorted_node_store.h" @@ -16,15 +15,6 @@ namespace SortedNodeStoreTypes { const uint16_t ChunkAlignment = 16; const uint32_t ChunkCompressed = 1 << 31; - std::atomic totalGroups; - std::atomic totalNodes; - std::atomic totalGroupSpace; - std::atomic totalAllocatedSpace; - std::atomic totalChunks; - std::atomic chunkSizeFreqs[257]; - std::atomic groupSizeFreqs[257]; - - // When SortedNodeStore first starts, it's not confident that it has seen an // entire segment, so it's in "collecting orphans" mode. Once it crosses a // threshold of 64K elements, it ceases to be in this mode. @@ -46,10 +36,7 @@ namespace SortedNodeStoreTypes { using namespace SortedNodeStoreTypes; SortedNodeStore::SortedNodeStore(bool compressNodes): compressNodes(compressNodes) { - // Each group can store 64K nodes. If we allocate 256K slots - // for groups, we support 2^34 = 17B nodes, or about twice - // the number used by OSM as of November 2023. - groups.resize(256 * 1024); + reopen(); } void SortedNodeStore::reopen() @@ -61,11 +48,16 @@ void SortedNodeStore::reopen() totalNodes = 0; totalGroups = 0; totalGroupSpace = 0; + totalAllocatedSpace = 0; totalChunks = 0; memset(chunkSizeFreqs, 0, sizeof(chunkSizeFreqs)); memset(groupSizeFreqs, 0, sizeof(groupSizeFreqs)); orphanage.clear(); workerBuffers.clear(); + + // Each group can store 64K nodes. If we allocate 256K slots + // for groups, we support 2^34 = 17B nodes, or about twice + // the number used by OSM as of November 2023. groups.clear(); groups.resize(256 * 1024); } diff --git a/src/sorted_way_store.cpp b/src/sorted_way_store.cpp index 8fdaa806..d0d05f00 100644 --- a/src/sorted_way_store.cpp +++ b/src/sorted_way_store.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -34,20 +33,12 @@ namespace SortedWayStoreTypes { thread_local int32_t int32Buffer[2000]; thread_local uint8_t uint8Buffer[8192]; - std::atomic totalWays; - std::atomic totalNodes; - std::atomic totalGroups; - std::atomic totalGroupSpace; - std::atomic totalChunks; } using namespace SortedWayStoreTypes; SortedWayStore::SortedWayStore(bool compressWays, const NodeStore& nodeStore): compressWays(compressWays), nodeStore(nodeStore) { - // Each group can store 64K ways. If we allocate 32K slots, - // we support 2^31 = 2B ways, or about twice the number used - // by OSM as of December 2023. - groups.resize(32 * 1024); + reopen(); } SortedWayStore::~SortedWayStore() { @@ -67,8 +58,12 @@ void SortedWayStore::reopen() { totalChunks = 0; orphanage.clear(); workerBuffers.clear(); + + // Each group can store 64K ways. If we allocate 32K slots, + // we support 2^31 = 2B ways, or about twice the number used + // by OSM as of December 2023. groups.clear(); - groups.resize(256 * 1024); + groups.resize(32 * 1024); } From 1c4174d21c61cde3c59c96332aaf413da6ed5902 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 10:29:12 -0500 Subject: [PATCH 25/81] SortedNodeStore: abstract TLS behind storage() Still only supports 1 class, but this is a step along the path. --- src/sorted_node_store.cpp | 127 +++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 56 deletions(-) diff --git a/src/sorted_node_store.cpp b/src/sorted_node_store.cpp index 0f856af6..0d7b43aa 100644 --- a/src/sorted_node_store.cpp +++ b/src/sorted_node_store.cpp @@ -15,26 +15,41 @@ namespace SortedNodeStoreTypes { const uint16_t ChunkAlignment = 16; const uint32_t ChunkCompressed = 1 << 31; - // When SortedNodeStore first starts, it's not confident that it has seen an - // entire segment, so it's in "collecting orphans" mode. Once it crosses a - // threshold of 64K elements, it ceases to be in this mode. - // - // Orphans are rounded up across multiple threads, and dealt with in - // the finalize step. - thread_local bool collectingOrphans = true; - thread_local uint64_t groupStart = -1; - thread_local std::vector* localNodes = nullptr; - - thread_local int64_t cachedChunk = -1; - thread_local std::vector cacheChunkLons; - thread_local std::vector cacheChunkLatps; - - thread_local uint32_t arenaSpace = 0; - thread_local char* arenaPtr = nullptr; + struct ThreadStorage { + ThreadStorage(): + collectingOrphans(true), + groupStart(-1), + localNodes(nullptr), + cachedChunk(-1), + arenaSpace(0), + arenaPtr(nullptr) {} + // When SortedNodeStore first starts, it's not confident that it has seen an + // entire segment, so it's in "collecting orphans" mode. Once it crosses a + // threshold of 64K elements, it ceases to be in this mode. + // + // Orphans are rounded up across multiple threads, and dealt with in + // the finalize step. + bool collectingOrphans = true; + uint64_t groupStart = -1; + std::vector* localNodes = nullptr; + + int64_t cachedChunk = -1; + std::vector cacheChunkLons; + std::vector cacheChunkLatps; + + uint32_t arenaSpace = 0; + char* arenaPtr = nullptr; + }; + + thread_local ThreadStorage threadStorage; } using namespace SortedNodeStoreTypes; +ThreadStorage& storage() { + return threadStorage; +} + SortedNodeStore::SortedNodeStore(bool compressNodes): compressNodes(compressNodes) { reopen(); } @@ -101,29 +116,29 @@ LatpLon SortedNodeStore::at(const NodeID id) const { size_t latpSize = (ptr->flags >> 10) & ((1 << 10) - 1); // TODO: we don't actually need the lonSize to decompress the data. // May as well store it as a sanity check for now. - size_t lonSize = ptr->flags & ((1 << 10) - 1); + // size_t lonSize = ptr->flags & ((1 << 10) - 1); size_t n = popcnt(ptr->nodeMask, 32) - 1; const size_t neededChunk = groupIndex * ChunkSize + chunk; // Really naive caching strategy - just cache the last-used chunk. // Probably good enough? - if (cachedChunk != neededChunk) { - cachedChunk = neededChunk; - cacheChunkLons.reserve(256); - cacheChunkLatps.reserve(256); + if (storage().cachedChunk != neededChunk) { + storage().cachedChunk = neededChunk; + storage().cacheChunkLons.reserve(256); + storage().cacheChunkLatps.reserve(256); uint8_t* latpData = ptr->data; uint8_t* lonData = ptr->data + latpSize; uint32_t recovdata[256] = {0}; streamvbyte_decode(latpData, recovdata, n); - cacheChunkLatps[0] = ptr->firstLatp; - zigzag_delta_decode(recovdata, &cacheChunkLatps[1], n, cacheChunkLatps[0]); + storage().cacheChunkLatps[0] = ptr->firstLatp; + zigzag_delta_decode(recovdata, &storage().cacheChunkLatps[1], n, storage().cacheChunkLatps[0]); streamvbyte_decode(lonData, recovdata, n); - cacheChunkLons[0] = ptr->firstLon; - zigzag_delta_decode(recovdata, &cacheChunkLons[1], n, cacheChunkLons[0]); + storage().cacheChunkLons[0] = ptr->firstLon; + zigzag_delta_decode(recovdata, &storage().cacheChunkLons[1], n, storage().cacheChunkLons[0]); } size_t nodeOffset = 0; @@ -134,7 +149,7 @@ LatpLon SortedNodeStore::at(const NodeID id) const { if (!(ptr->nodeMask[nodeMaskByte] & (1 << nodeMaskBit))) throw std::out_of_range("SortedNodeStore: node " + std::to_string(id) + " missing, no node"); - return { cacheChunkLatps[nodeOffset], cacheChunkLons[nodeOffset] }; + return { storage().cacheChunkLatps[nodeOffset], storage().cacheChunkLons[nodeOffset] }; } UncompressedChunkInfo* ptr = (UncompressedChunkInfo*)basePtr; @@ -176,58 +191,58 @@ size_t SortedNodeStore::size() const { } void SortedNodeStore::insert(const std::vector& elements) { - if (localNodes == nullptr) { + if (storage().localNodes == nullptr) { std::lock_guard lock(orphanageMutex); if (workerBuffers.size() == 0) workerBuffers.reserve(256); else if (workerBuffers.size() == workerBuffers.capacity()) throw std::runtime_error("SortedNodeStore doesn't support more than 256 cores"); workerBuffers.push_back(std::vector()); - localNodes = &workerBuffers.back(); + storage().localNodes = &workerBuffers.back(); } - if (groupStart == -1) { + if (storage().groupStart == -1) { // Mark where the first full group starts, so we know when to transition // out of collecting orphans. - groupStart = elements[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + storage().groupStart = elements[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } int i = 0; - while (collectingOrphans && i < elements.size()) { + while (storage().collectingOrphans && i < elements.size()) { const element_t& el = elements[i]; - if (el.first >= groupStart + (GroupSize * ChunkSize)) { - collectingOrphans = false; + if (el.first >= storage().groupStart + (GroupSize * ChunkSize)) { + storage().collectingOrphans = false; // Calculate new groupStart, rounding to previous boundary. - groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); - collectOrphans(*localNodes); - localNodes->clear(); + storage().groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + collectOrphans(*storage().localNodes); + storage().localNodes->clear(); } - localNodes->push_back(el); + storage().localNodes->push_back(el); i++; } while(i < elements.size()) { const element_t& el = elements[i]; - if (el.first >= groupStart + (GroupSize * ChunkSize)) { - publishGroup(*localNodes); - localNodes->clear(); - groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + if (el.first >= storage().groupStart + (GroupSize * ChunkSize)) { + publishGroup(*storage().localNodes); + storage().localNodes->clear(); + storage().groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } - localNodes->push_back(el); + storage().localNodes->push_back(el); i++; } } void SortedNodeStore::batchStart() { - collectingOrphans = true; - groupStart = -1; - if (localNodes == nullptr || localNodes->size() == 0) + storage().collectingOrphans = true; + storage().groupStart = -1; + if (storage().localNodes == nullptr || storage().localNodes->size() == 0) return; - collectOrphans(*localNodes); - localNodes->clear(); + collectOrphans(*storage().localNodes); + storage().localNodes->clear(); } void SortedNodeStore::finalize(size_t threadNum) { @@ -402,22 +417,22 @@ void SortedNodeStore::publishGroup(const std::vector& nodes) { GroupInfo* groupInfo = nullptr; - if (arenaSpace < groupSpace) { + if (storage().arenaSpace < groupSpace) { // A full group takes ~330KB. Nodes are read _fast_, and there ends // up being contention calling the allocator when reading the // planet on a machine with 48 cores -- so allocate in large chunks. - arenaSpace = 4 * 1024 * 1024; - totalAllocatedSpace += arenaSpace; - arenaPtr = (char*)void_mmap_allocator::allocate(arenaSpace); - if (arenaPtr == nullptr) + storage().arenaSpace = 4 * 1024 * 1024; + totalAllocatedSpace += storage().arenaSpace; + storage().arenaPtr = (char*)void_mmap_allocator::allocate(storage().arenaSpace); + if (storage().arenaPtr == nullptr) throw std::runtime_error("SortedNodeStore: failed to allocate arena"); std::lock_guard lock(orphanageMutex); - allocatedMemory.push_back(std::make_pair((void*)arenaPtr, arenaSpace)); + allocatedMemory.push_back(std::make_pair((void*)storage().arenaPtr, storage().arenaSpace)); } - arenaSpace -= groupSpace; - groupInfo = (GroupInfo*)arenaPtr; - arenaPtr += groupSpace; + storage().arenaSpace -= groupSpace; + groupInfo = (GroupInfo*)storage().arenaPtr; + storage().arenaPtr += groupSpace; if (groups[groupIndex] != nullptr) throw std::runtime_error("SortedNodeStore: group already present"); From 99b5912524d015e59ecc94ac36b22a662fc2ee20 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 10:42:09 -0500 Subject: [PATCH 26/81] SortedWayStore: abstract TLS behind storage() --- src/sorted_node_store.cpp | 8 ++--- src/sorted_way_store.cpp | 71 +++++++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/sorted_node_store.cpp b/src/sorted_node_store.cpp index 0d7b43aa..2f079755 100644 --- a/src/sorted_node_store.cpp +++ b/src/sorted_node_store.cpp @@ -42,14 +42,14 @@ namespace SortedNodeStoreTypes { }; thread_local ThreadStorage threadStorage; + + ThreadStorage& storage() { + return threadStorage; + } } using namespace SortedNodeStoreTypes; -ThreadStorage& storage() { - return threadStorage; -} - SortedNodeStore::SortedNodeStore(bool compressNodes): compressNodes(compressNodes) { reopen(); } diff --git a/src/sorted_way_store.cpp b/src/sorted_way_store.cpp index d0d05f00..f87dde21 100644 --- a/src/sorted_way_store.cpp +++ b/src/sorted_way_store.cpp @@ -18,21 +18,35 @@ namespace SortedWayStoreTypes { const uint16_t ClosedWay = 1 << 14; const uint16_t UniformUpperBits = 1 << 13; - thread_local bool collectingOrphans = true; - thread_local uint64_t groupStart = -1; - thread_local std::vector>>* localWays = NULL; + struct ThreadStorage { + ThreadStorage(): + collectingOrphans(true), + groupStart(-1), + localWays(nullptr) {} + + bool collectingOrphans; + uint64_t groupStart; + std::vector>>* localWays; + std::vector encodedWay; + }; - thread_local std::vector encodedWay; + thread_local ThreadStorage threadStorage; // C++ doesn't support variable length arrays declared on stack. // g++ and clang support it, but msvc doesn't. Rather than pay the // cost of a vector for every decode, we use a thread_local with room for at // least 2,000 nodes. + // + // Note: these are scratch buffers, so they remain as true thread-locals, + // and aren't part of ThreadStorage. thread_local uint64_t highBytes[2000]; thread_local uint32_t uint32Buffer[2000]; thread_local int32_t int32Buffer[2000]; thread_local uint8_t uint8Buffer[8192]; + ThreadStorage& storage() { + return threadStorage; + } } using namespace SortedWayStoreTypes; @@ -141,46 +155,46 @@ const void SortedWayStore::insertNodes(const std::vector lock(orphanageMutex); if (workerBuffers.size() == 0) workerBuffers.reserve(256); else if (workerBuffers.size() == workerBuffers.capacity()) throw std::runtime_error("SortedWayStore doesn't support more than 256 cores"); workerBuffers.push_back(std::vector>>()); - localWays = &workerBuffers.back(); + storage().localWays = &workerBuffers.back(); } - if (groupStart == -1) { + if (storage().groupStart == -1) { // Mark where the first full group starts, so we know when to transition // out of collecting orphans. - groupStart = newWays[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + storage().groupStart = newWays[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } int i = 0; - while (collectingOrphans && i < newWays.size()) { + while (storage().collectingOrphans && i < newWays.size()) { const auto& el = newWays[i]; - if (el.first >= groupStart + (GroupSize * ChunkSize)) { - collectingOrphans = false; + if (el.first >= storage().groupStart + (GroupSize * ChunkSize)) { + storage().collectingOrphans = false; // Calculate new groupStart, rounding to previous boundary. - groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); - collectOrphans(*localWays); - localWays->clear(); + storage().groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + collectOrphans(*storage().localWays); + storage().localWays->clear(); } - localWays->push_back(el); + storage().localWays->push_back(el); i++; } while(i < newWays.size()) { const auto& el = newWays[i]; - if (el.first >= groupStart + (GroupSize * ChunkSize)) { - publishGroup(*localWays); - localWays->clear(); - groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + if (el.first >= storage().groupStart + (GroupSize * ChunkSize)) { + publishGroup(*storage().localWays); + storage().localWays->clear(); + storage().groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } - localWays->push_back(el); + storage().localWays->push_back(el); i++; } } @@ -224,13 +238,13 @@ void SortedWayStore::finalize(unsigned int threadNum) { } void SortedWayStore::batchStart() { - collectingOrphans = true; - groupStart = -1; - if (localWays == nullptr || localWays->size() == 0) + storage().collectingOrphans = true; + storage().groupStart = -1; + if (storage().localWays == nullptr || storage().localWays->size() == 0) return; - collectOrphans(*localWays); - localWays->clear(); + collectOrphans(*storage().localWays); + storage().localWays->clear(); } void SortedWayStore::collectOrphans(const std::vector>>& orphans) { @@ -279,7 +293,6 @@ std::vector SortedWayStore::decodeWay(uint16_t flags, const uint8_t* inp for (int i = 0; i < length; i++) rv.push_back(highBytes[i] | lowIntData[i]); } else { - uint16_t compressedLength = *(uint16_t*)input; input += 2; uint32_t firstInt = *(uint32_t*)(input); @@ -446,12 +459,12 @@ void SortedWayStore::publishGroup(const std::vectorwayIds.push_back(id % ChunkSize); - uint16_t flags = encodeWay(way.second, encodedWay, compressWays && way.second.size() >= 4); + uint16_t flags = encodeWay(way.second, storage().encodedWay, compressWays && way.second.size() >= 4); lastChunk->wayFlags.push_back(flags); std::vector encoded; - encoded.resize(encodedWay.size()); - memcpy(encoded.data(), encodedWay.data(), encodedWay.size()); + encoded.resize(storage().encodedWay.size()); + memcpy(encoded.data(), storage().encodedWay.data(), storage().encodedWay.size()); lastChunk->encodedWays.push_back(std::move(encoded)); } From f225ebdb8d44f25624162ef17bd7b5f652e33c15 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 10:54:42 -0500 Subject: [PATCH 27/81] SortedNodeStore: support multiple instances --- src/sorted_node_store.cpp | 91 ++++++++++++++++++--------------- test/sorted_node_store.test.cpp | 20 +++++--- 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/src/sorted_node_store.cpp b/src/sorted_node_store.cpp index 2f079755..0194ca8a 100644 --- a/src/sorted_node_store.cpp +++ b/src/sorted_node_store.cpp @@ -41,10 +41,17 @@ namespace SortedNodeStoreTypes { char* arenaPtr = nullptr; }; - thread_local ThreadStorage threadStorage; + thread_local std::deque> threadStorage; - ThreadStorage& storage() { - return threadStorage; + ThreadStorage& s(const SortedNodeStore* who) { + for (auto& entry : threadStorage) + if (entry.first == who) + return entry.second; + + threadStorage.push_back(std::make_pair(who, ThreadStorage())); + + auto& rv = threadStorage.back(); + return rv.second; } } @@ -123,22 +130,22 @@ LatpLon SortedNodeStore::at(const NodeID id) const { // Really naive caching strategy - just cache the last-used chunk. // Probably good enough? - if (storage().cachedChunk != neededChunk) { - storage().cachedChunk = neededChunk; - storage().cacheChunkLons.reserve(256); - storage().cacheChunkLatps.reserve(256); + if (s(this).cachedChunk != neededChunk) { + s(this).cachedChunk = neededChunk; + s(this).cacheChunkLons.reserve(256); + s(this).cacheChunkLatps.reserve(256); uint8_t* latpData = ptr->data; uint8_t* lonData = ptr->data + latpSize; uint32_t recovdata[256] = {0}; streamvbyte_decode(latpData, recovdata, n); - storage().cacheChunkLatps[0] = ptr->firstLatp; - zigzag_delta_decode(recovdata, &storage().cacheChunkLatps[1], n, storage().cacheChunkLatps[0]); + s(this).cacheChunkLatps[0] = ptr->firstLatp; + zigzag_delta_decode(recovdata, &s(this).cacheChunkLatps[1], n, s(this).cacheChunkLatps[0]); streamvbyte_decode(lonData, recovdata, n); - storage().cacheChunkLons[0] = ptr->firstLon; - zigzag_delta_decode(recovdata, &storage().cacheChunkLons[1], n, storage().cacheChunkLons[0]); + s(this).cacheChunkLons[0] = ptr->firstLon; + zigzag_delta_decode(recovdata, &s(this).cacheChunkLons[1], n, s(this).cacheChunkLons[0]); } size_t nodeOffset = 0; @@ -149,7 +156,7 @@ LatpLon SortedNodeStore::at(const NodeID id) const { if (!(ptr->nodeMask[nodeMaskByte] & (1 << nodeMaskBit))) throw std::out_of_range("SortedNodeStore: node " + std::to_string(id) + " missing, no node"); - return { storage().cacheChunkLatps[nodeOffset], storage().cacheChunkLons[nodeOffset] }; + return { s(this).cacheChunkLatps[nodeOffset], s(this).cacheChunkLons[nodeOffset] }; } UncompressedChunkInfo* ptr = (UncompressedChunkInfo*)basePtr; @@ -191,58 +198,58 @@ size_t SortedNodeStore::size() const { } void SortedNodeStore::insert(const std::vector& elements) { - if (storage().localNodes == nullptr) { + if (s(this).localNodes == nullptr) { std::lock_guard lock(orphanageMutex); if (workerBuffers.size() == 0) workerBuffers.reserve(256); else if (workerBuffers.size() == workerBuffers.capacity()) throw std::runtime_error("SortedNodeStore doesn't support more than 256 cores"); workerBuffers.push_back(std::vector()); - storage().localNodes = &workerBuffers.back(); + s(this).localNodes = &workerBuffers.back(); } - if (storage().groupStart == -1) { + if (s(this).groupStart == -1) { // Mark where the first full group starts, so we know when to transition // out of collecting orphans. - storage().groupStart = elements[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + s(this).groupStart = elements[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } int i = 0; - while (storage().collectingOrphans && i < elements.size()) { + while (s(this).collectingOrphans && i < elements.size()) { const element_t& el = elements[i]; - if (el.first >= storage().groupStart + (GroupSize * ChunkSize)) { - storage().collectingOrphans = false; + if (el.first >= s(this).groupStart + (GroupSize * ChunkSize)) { + s(this).collectingOrphans = false; // Calculate new groupStart, rounding to previous boundary. - storage().groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); - collectOrphans(*storage().localNodes); - storage().localNodes->clear(); + s(this).groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + collectOrphans(*s(this).localNodes); + s(this).localNodes->clear(); } - storage().localNodes->push_back(el); + s(this).localNodes->push_back(el); i++; } while(i < elements.size()) { const element_t& el = elements[i]; - if (el.first >= storage().groupStart + (GroupSize * ChunkSize)) { - publishGroup(*storage().localNodes); - storage().localNodes->clear(); - storage().groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + if (el.first >= s(this).groupStart + (GroupSize * ChunkSize)) { + publishGroup(*s(this).localNodes); + s(this).localNodes->clear(); + s(this).groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } - storage().localNodes->push_back(el); + s(this).localNodes->push_back(el); i++; } } void SortedNodeStore::batchStart() { - storage().collectingOrphans = true; - storage().groupStart = -1; - if (storage().localNodes == nullptr || storage().localNodes->size() == 0) + s(this).collectingOrphans = true; + s(this).groupStart = -1; + if (s(this).localNodes == nullptr || s(this).localNodes->size() == 0) return; - collectOrphans(*storage().localNodes); - storage().localNodes->clear(); + collectOrphans(*s(this).localNodes); + s(this).localNodes->clear(); } void SortedNodeStore::finalize(size_t threadNum) { @@ -417,22 +424,22 @@ void SortedNodeStore::publishGroup(const std::vector& nodes) { GroupInfo* groupInfo = nullptr; - if (storage().arenaSpace < groupSpace) { + if (s(this).arenaSpace < groupSpace) { // A full group takes ~330KB. Nodes are read _fast_, and there ends // up being contention calling the allocator when reading the // planet on a machine with 48 cores -- so allocate in large chunks. - storage().arenaSpace = 4 * 1024 * 1024; - totalAllocatedSpace += storage().arenaSpace; - storage().arenaPtr = (char*)void_mmap_allocator::allocate(storage().arenaSpace); - if (storage().arenaPtr == nullptr) + s(this).arenaSpace = 4 * 1024 * 1024; + totalAllocatedSpace += s(this).arenaSpace; + s(this).arenaPtr = (char*)void_mmap_allocator::allocate(s(this).arenaSpace); + if (s(this).arenaPtr == nullptr) throw std::runtime_error("SortedNodeStore: failed to allocate arena"); std::lock_guard lock(orphanageMutex); - allocatedMemory.push_back(std::make_pair((void*)storage().arenaPtr, storage().arenaSpace)); + allocatedMemory.push_back(std::make_pair((void*)s(this).arenaPtr, s(this).arenaSpace)); } - storage().arenaSpace -= groupSpace; - groupInfo = (GroupInfo*)storage().arenaPtr; - storage().arenaPtr += groupSpace; + s(this).arenaSpace -= groupSpace; + groupInfo = (GroupInfo*)s(this).arenaPtr; + s(this).arenaPtr += groupSpace; if (groups[groupIndex] != nullptr) throw std::runtime_error("SortedNodeStore: group already present"); diff --git a/test/sorted_node_store.test.cpp b/test/sorted_node_store.test.cpp index ea6956d6..ba7edb2d 100644 --- a/test/sorted_node_store.test.cpp +++ b/test/sorted_node_store.test.cpp @@ -3,17 +3,23 @@ #include "sorted_node_store.h" MU_TEST(test_sorted_node_store) { - SortedNodeStore sns(true); - mu_check(sns.size() == 0); + SortedNodeStore s1(true), s2(true); + mu_check(s1.size() == 0); + mu_check(s2.size() == 0); - sns.batchStart(); + s1.batchStart(); + s2.batchStart(); - sns.insert({ {1, {2, 3 } } }); + s1.insert({ {1, {2, 3 } } }); + s2.insert({ {2, {3, 4 } } }); - sns.finalize(1); - - mu_check(sns.size() == 1); + s1.finalize(1); + s2.finalize(1); + mu_check(s1.size() == 1); + mu_check(s1.at(1) == LatpLon({2, 3})); + mu_check(s2.size() == 1); + mu_check(s2.at(2) == LatpLon({3, 4})); } MU_TEST_SUITE(test_suite_sorted_node_store) { From 6c7917b996b79275549253d3f77eb1337f7c6652 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 11:17:13 -0500 Subject: [PATCH 28/81] SortedWayStorage: support multiple instances --- src/sorted_node_store.cpp | 2 + src/sorted_way_store.cpp | 68 +++++++++++++++++++--------------- test/sorted_way_store.test.cpp | 17 +++++++++ 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/sorted_node_store.cpp b/src/sorted_node_store.cpp index 0194ca8a..6761ad00 100644 --- a/src/sorted_node_store.cpp +++ b/src/sorted_node_store.cpp @@ -87,6 +87,8 @@ void SortedNodeStore::reopen() SortedNodeStore::~SortedNodeStore() { for (const auto entry: allocatedMemory) void_mmap_allocator::deallocate(entry.first, entry.second); + + s(this) = ThreadStorage(); } LatpLon SortedNodeStore::at(const NodeID id) const { diff --git a/src/sorted_way_store.cpp b/src/sorted_way_store.cpp index f87dde21..c1dd9ae2 100644 --- a/src/sorted_way_store.cpp +++ b/src/sorted_way_store.cpp @@ -30,7 +30,18 @@ namespace SortedWayStoreTypes { std::vector encodedWay; }; - thread_local ThreadStorage threadStorage; + thread_local std::deque> threadStorage; + + ThreadStorage& s(const SortedWayStore* who) { + for (auto& entry : threadStorage) + if (entry.first == who) + return entry.second; + + threadStorage.push_back(std::make_pair(who, ThreadStorage())); + + auto& rv = threadStorage.back(); + return rv.second; + } // C++ doesn't support variable length arrays declared on stack. // g++ and clang support it, but msvc doesn't. Rather than pay the @@ -43,10 +54,6 @@ namespace SortedWayStoreTypes { thread_local uint32_t uint32Buffer[2000]; thread_local int32_t int32Buffer[2000]; thread_local uint8_t uint8Buffer[8192]; - - ThreadStorage& storage() { - return threadStorage; - } } using namespace SortedWayStoreTypes; @@ -58,6 +65,8 @@ SortedWayStore::SortedWayStore(bool compressWays, const NodeStore& nodeStore): c SortedWayStore::~SortedWayStore() { for (const auto entry: allocatedMemory) void_mmap_allocator::deallocate(entry.first, entry.second); + + s(this) = ThreadStorage(); } void SortedWayStore::reopen() { @@ -155,46 +164,46 @@ const void SortedWayStore::insertNodes(const std::vector lock(orphanageMutex); if (workerBuffers.size() == 0) workerBuffers.reserve(256); else if (workerBuffers.size() == workerBuffers.capacity()) throw std::runtime_error("SortedWayStore doesn't support more than 256 cores"); workerBuffers.push_back(std::vector>>()); - storage().localWays = &workerBuffers.back(); + s(this).localWays = &workerBuffers.back(); } - if (storage().groupStart == -1) { + if (s(this).groupStart == -1) { // Mark where the first full group starts, so we know when to transition // out of collecting orphans. - storage().groupStart = newWays[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + s(this).groupStart = newWays[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } int i = 0; - while (storage().collectingOrphans && i < newWays.size()) { + while (s(this).collectingOrphans && i < newWays.size()) { const auto& el = newWays[i]; - if (el.first >= storage().groupStart + (GroupSize * ChunkSize)) { - storage().collectingOrphans = false; + if (el.first >= s(this).groupStart + (GroupSize * ChunkSize)) { + s(this).collectingOrphans = false; // Calculate new groupStart, rounding to previous boundary. - storage().groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); - collectOrphans(*storage().localWays); - storage().localWays->clear(); + s(this).groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + collectOrphans(*s(this).localWays); + s(this).localWays->clear(); } - storage().localWays->push_back(el); + s(this).localWays->push_back(el); i++; } while(i < newWays.size()) { const auto& el = newWays[i]; - if (el.first >= storage().groupStart + (GroupSize * ChunkSize)) { - publishGroup(*storage().localWays); - storage().localWays->clear(); - storage().groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + if (el.first >= s(this).groupStart + (GroupSize * ChunkSize)) { + publishGroup(*s(this).localWays); + s(this).localWays->clear(); + s(this).groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } - storage().localWays->push_back(el); + s(this).localWays->push_back(el); i++; } } @@ -238,13 +247,13 @@ void SortedWayStore::finalize(unsigned int threadNum) { } void SortedWayStore::batchStart() { - storage().collectingOrphans = true; - storage().groupStart = -1; - if (storage().localWays == nullptr || storage().localWays->size() == 0) + s(this).collectingOrphans = true; + s(this).groupStart = -1; + if (s(this).localWays == nullptr || s(this).localWays->size() == 0) return; - collectOrphans(*storage().localWays); - storage().localWays->clear(); + collectOrphans(*s(this).localWays); + s(this).localWays->clear(); } void SortedWayStore::collectOrphans(const std::vector>>& orphans) { @@ -253,6 +262,7 @@ void SortedWayStore::collectOrphans(const std::vector>>& vec = orphanage[groupIndex]; const size_t i = vec.size(); + vec.resize(i + orphans.size()); std::copy(orphans.begin(), orphans.end(), vec.begin() + i); } @@ -459,12 +469,12 @@ void SortedWayStore::publishGroup(const std::vectorwayIds.push_back(id % ChunkSize); - uint16_t flags = encodeWay(way.second, storage().encodedWay, compressWays && way.second.size() >= 4); + uint16_t flags = encodeWay(way.second, s(this).encodedWay, compressWays && way.second.size() >= 4); lastChunk->wayFlags.push_back(flags); std::vector encoded; - encoded.resize(storage().encodedWay.size()); - memcpy(encoded.data(), storage().encodedWay.data(), storage().encodedWay.size()); + encoded.resize(s(this).encodedWay.size()); + memcpy(encoded.data(), s(this).encodedWay.data(), s(this).encodedWay.size()); lastChunk->encodedWays.push_back(std::move(encoded)); } diff --git a/test/sorted_way_store.test.cpp b/test/sorted_way_store.test.cpp index 217a1110..8c4c432d 100644 --- a/test/sorted_way_store.test.cpp +++ b/test/sorted_way_store.test.cpp @@ -74,6 +74,22 @@ MU_TEST(test_encode_way) { } } +MU_TEST(test_multiple_stores) { + TestNodeStore ns; + SortedWayStore s1(true, ns), s2(true, ns); + s1.batchStart(); + s2.batchStart(); + + s1.insertNodes({{ 1, { 1 } }}); + s2.insertNodes({{ 2, { 2 } }}); + + s1.finalize(1); + s2.finalize(1); + + mu_check(s1.size() == 1); + mu_check(s2.size() == 1); +} + MU_TEST(test_way_store) { TestNodeStore ns; SortedWayStore sws(true, ns); @@ -182,6 +198,7 @@ MU_TEST(test_populate_mask) { MU_TEST_SUITE(test_suite_sorted_way_store) { MU_RUN_TEST(test_encode_way); + MU_RUN_TEST(test_multiple_stores); MU_RUN_TEST(test_way_store); } From 5d9ca2b8fdba64f6616f111bf51ed43ba66b575e Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 11:40:43 -0500 Subject: [PATCH 29/81] actually fix the low zoom object collection D'oh, this "worked" due to two bugs cancelling each other: (a) the code to find things in the low zoom list never found anything, because it assumed a base z6 tile of 0/0 (b) we weren't returning early, so the normal code still ran Rejigged to actually do what I was intending --- include/tile_data.h | 53 ++++++++++++++++++++++++++++++++++++++------- src/tile_data.cpp | 7 +++--- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/include/tile_data.h b/include/tile_data.h index 78793c27..b5641ace 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -52,8 +52,9 @@ template void finalizeObjects( const unsigned int& baseZoom, typename std::vector>::iterator begin, typename std::vector>::iterator end, - typename std::vector>& lowZoom + typename AppendVectorNS::AppendVector>& lowZoom ) { + size_t z6OffsetDivisor = baseZoom >= CLUSTER_ZOOM ? (1 << (baseZoom - CLUSTER_ZOOM)) : 1; #ifdef CLOCK_MONOTONIC timespec startTs, endTs; clock_gettime(CLOCK_MONOTONIC, &startTs); @@ -75,9 +76,20 @@ template void finalizeObjects( if (it->size() == 0) continue; + // We track a separate copy of low zoom objects to avoid scanning large + // lists of objects that may be on slow disk storage. for (auto objectIt = it->begin(); objectIt != it->end(); objectIt++) - if (objectIt->oo.minZoom < CLUSTER_ZOOM) - lowZoom[0].push_back(*objectIt); + if (objectIt->oo.minZoom < CLUSTER_ZOOM) { + size_t z6x = i / CLUSTER_ZOOM_WIDTH; + size_t z6y = i % CLUSTER_ZOOM_WIDTH; + lowZoom.push_back(std::make_pair( + TileCoordinates( + z6OffsetDivisor * z6x + objectIt->x, + z6OffsetDivisor * z6y + objectIt->y + ), + *objectIt + )); + } // If the user is doing a a small extract, there are few populated // entries in `object`. @@ -172,6 +184,31 @@ inline OutputObjectID outputObjectWithId(const OutputObjectXYI return OutputObjectID({ input.oo, input.id }); } +template void collectLowZoomObjectsForTile( + const unsigned int& baseZoom, + typename AppendVectorNS::AppendVector> objects, + unsigned int zoom, + const TileCoordinates& dstIndex, + std::vector& output +) { + for (size_t j = 0; j < objects.size(); j++) { + const auto& object = objects[j]; + + TileCoordinate baseX = object.first.x; + TileCoordinate baseY = object.first.y; + + // Translate the x, y at the requested zoom level + TileCoordinate x = baseX / (1 << (baseZoom - zoom)); + TileCoordinate y = baseY / (1 << (baseZoom - zoom)); + + if (dstIndex.x == x && dstIndex.y == y) { + if (object.second.oo.minZoom <= zoom) { + output.push_back(outputObjectWithId(object.second)); + } + } + } +} + template void collectObjectsForTileTemplate( const unsigned int& baseZoom, typename std::vector>::iterator objects, @@ -184,9 +221,6 @@ template void collectObjectsForTileTemplate( uint16_t z6OffsetDivisor = baseZoom >= CLUSTER_ZOOM ? (1 << (baseZoom - CLUSTER_ZOOM)) : 1; for (size_t i = iStart; i < iEnd; i++) { - const size_t z6x = i / CLUSTER_ZOOM_WIDTH; - const size_t z6y = i % CLUSTER_ZOOM_WIDTH; - if (zoom >= CLUSTER_ZOOM) { // If z >= 6, we can compute the exact bounds within the objects array. // Translate to the base zoom, then do a binary search to find @@ -258,6 +292,9 @@ template void collectObjectsForTileTemplate( } } else { + const size_t z6x = i / CLUSTER_ZOOM_WIDTH; + const size_t z6y = i % CLUSTER_ZOOM_WIDTH; + for (size_t j = 0; j < objects[i].size(); j++) { // Compute the x, y at the base zoom level TileCoordinate baseX = z6x * z6OffsetDivisor + objects[i][j].x; @@ -318,9 +355,9 @@ class TileDataSource { // If config.include_ids is true, objectsWithIds will be populated. // Otherwise, objects. std::vector> objects; - std::vector> lowZoomObjects; + AppendVectorNS::AppendVector> lowZoomObjects; std::vector> objectsWithIds; - std::vector> lowZoomObjectsWithIds; + AppendVectorNS::AppendVector> lowZoomObjectsWithIds; // rtree index of large objects using oo_rtree_param_type = boost::geometry::index::quadratic<128>; diff --git a/src/tile_data.cpp b/src/tile_data.cpp index d3fc15c2..b56e97e7 100644 --- a/src/tile_data.cpp +++ b/src/tile_data.cpp @@ -47,9 +47,7 @@ TileDataSource::TileDataSource(size_t threadNum, unsigned int baseZoom, bool inc z6OffsetDivisor(baseZoom >= CLUSTER_ZOOM ? (1 << (baseZoom - CLUSTER_ZOOM)) : 1), objectsMutex(threadNum * 4), objects(CLUSTER_ZOOM_AREA), - lowZoomObjects(1), objectsWithIds(CLUSTER_ZOOM_AREA), - lowZoomObjectsWithIds(1), baseZoom(baseZoom), pointStores(threadNum), linestringStores(threadNum), @@ -143,8 +141,9 @@ void TileDataSource::collectObjectsForTile( std::vector& output ) { if (zoom < CLUSTER_ZOOM) { - collectObjectsForTileTemplate(baseZoom, lowZoomObjects.begin(), 0, 1, zoom, dstIndex, output); - collectObjectsForTileTemplate(baseZoom, lowZoomObjectsWithIds.begin(), 0, 1, zoom, dstIndex, output); + collectLowZoomObjectsForTile(baseZoom, lowZoomObjects, zoom, dstIndex, output); + collectLowZoomObjectsForTile(baseZoom, lowZoomObjectsWithIds, zoom, dstIndex, output); + return; } size_t iStart = 0; From 24b73f1f434d28098ddd68150aad2da75ed139b3 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 12:53:39 -0500 Subject: [PATCH 30/81] AppendVector tweaks --- include/append_vector.h | 4 +++- test/append_vector.test.cpp | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/include/append_vector.h b/include/append_vector.h index 3fe9b907..07531217 100644 --- a/include/append_vector.h +++ b/include/append_vector.h @@ -158,7 +158,9 @@ namespace AppendVectorNS { } T& operator [](int idx) { - return vecs[idx / APPEND_VECTOR_SIZE][idx % APPEND_VECTOR_SIZE]; + auto& vec = vecs[idx / APPEND_VECTOR_SIZE]; + auto& el = vec[idx % APPEND_VECTOR_SIZE]; + return el; } Iterator begin() { diff --git a/test/append_vector.test.cpp b/test/append_vector.test.cpp index 300f6e30..db4949e2 100644 --- a/test/append_vector.test.cpp +++ b/test/append_vector.test.cpp @@ -6,8 +6,11 @@ using namespace AppendVectorNS; MU_TEST(test_append_vector) { - AppendVector vec; + AppendVector vec; + AppendVector vec2; mu_check(vec.size() == 0); + mu_check(vec.begin() == vec.end()); + mu_check(vec.begin() != vec2.begin()); for (int i = 0; i < 10000; i++) { vec.push_back(i); @@ -16,7 +19,7 @@ MU_TEST(test_append_vector) { mu_check(vec[25] == 25); - const AppendVector::Iterator& it = vec.begin(); + const AppendVector::Iterator& it = vec.begin(); mu_check(*it == 0); mu_check(*(it + 1) == 1); mu_check(*(it + 2) == 2); @@ -52,7 +55,7 @@ MU_TEST(test_append_vector) { vec.begin(), vec.end(), 123, - [](const uint32_t& a, const uint32_t& toFind) { + [](const int32_t& a, const int32_t& toFind) { return a < toFind; } ); @@ -64,13 +67,23 @@ MU_TEST(test_append_vector) { vec.begin(), vec.end(), 123123, - [](const uint32_t& a, const uint32_t& toFind) { + [](const int32_t& a, const int32_t& toFind) { return a < toFind; } ); mu_check(iter == vec.end()); + iter = std::lower_bound( + vec.begin(), + vec.end(), + -2, + [](const int32_t& a, const int32_t& toFind) { + return a < toFind; + } + ); + + mu_check(iter == vec.begin()); } MU_TEST_SUITE(test_suite_append_vector) { From 2a053652cf3d14f91b45e695213d637277dbe525 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 13:01:09 -0500 Subject: [PATCH 31/81] more low zoom fixes --- include/tile_data.h | 209 ++++++++++++++++++++------------------------ src/tile_data.cpp | 4 +- 2 files changed, 98 insertions(+), 115 deletions(-) diff --git a/include/tile_data.h b/include/tile_data.h index b5641ace..804761c8 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -52,7 +52,7 @@ template void finalizeObjects( const unsigned int& baseZoom, typename std::vector>::iterator begin, typename std::vector>::iterator end, - typename AppendVectorNS::AppendVector>& lowZoom + typename std::vector>& lowZoom ) { size_t z6OffsetDivisor = baseZoom >= CLUSTER_ZOOM ? (1 << (baseZoom - CLUSTER_ZOOM)) : 1; #ifdef CLOCK_MONOTONIC @@ -60,11 +60,11 @@ template void finalizeObjects( clock_gettime(CLOCK_MONOTONIC, &startTs); #endif - int i = 0; + int i = -1; for (auto it = begin; it != end; it++) { i++; - if (i % 10 == 0 || i == 4096) { - std::cout << "\r" << name << ": finalizing z6 tile " << i << "/" << CLUSTER_ZOOM_AREA; + if (it->size() > 0 || i % 10 == 0 || i == 4095) { + std::cout << "\r" << name << ": finalizing z6 tile " << (i + 1) << "/" << CLUSTER_ZOOM_AREA; #ifdef CLOCK_MONOTONIC clock_gettime(CLOCK_MONOTONIC, &endTs); @@ -79,17 +79,8 @@ template void finalizeObjects( // We track a separate copy of low zoom objects to avoid scanning large // lists of objects that may be on slow disk storage. for (auto objectIt = it->begin(); objectIt != it->end(); objectIt++) - if (objectIt->oo.minZoom < CLUSTER_ZOOM) { - size_t z6x = i / CLUSTER_ZOOM_WIDTH; - size_t z6y = i % CLUSTER_ZOOM_WIDTH; - lowZoom.push_back(std::make_pair( - TileCoordinates( - z6OffsetDivisor * z6x + objectIt->x, - z6OffsetDivisor * z6y + objectIt->y - ), - *objectIt - )); - } + if (objectIt->oo.minZoom < CLUSTER_ZOOM) + lowZoom[i].push_back(*objectIt); // If the user is doing a a small extract, there are few populated // entries in `object`. @@ -186,24 +177,33 @@ inline OutputObjectID outputObjectWithId(const OutputObjectXYI template void collectLowZoomObjectsForTile( const unsigned int& baseZoom, - typename AppendVectorNS::AppendVector> objects, + typename std::vector> objects, unsigned int zoom, const TileCoordinates& dstIndex, std::vector& output ) { - for (size_t j = 0; j < objects.size(); j++) { - const auto& object = objects[j]; + if (zoom >= CLUSTER_ZOOM) + throw std::runtime_error("collectLowZoomObjectsForTile should not be called for high zooms"); + + uint16_t z6OffsetDivisor = baseZoom >= CLUSTER_ZOOM ? (1 << (baseZoom - CLUSTER_ZOOM)) : 1; + + for (size_t i = 0; i < objects.size(); i++) { + const size_t z6x = i / CLUSTER_ZOOM_WIDTH; + const size_t z6y = i % CLUSTER_ZOOM_WIDTH; - TileCoordinate baseX = object.first.x; - TileCoordinate baseY = object.first.y; + for (size_t j = 0; j < objects[i].size(); j++) { + // Compute the x, y at the base zoom level + TileCoordinate baseX = z6x * z6OffsetDivisor + objects[i][j].x; + TileCoordinate baseY = z6y * z6OffsetDivisor + objects[i][j].y; - // Translate the x, y at the requested zoom level - TileCoordinate x = baseX / (1 << (baseZoom - zoom)); - TileCoordinate y = baseY / (1 << (baseZoom - zoom)); + // Translate the x, y at the requested zoom level + TileCoordinate x = baseX / (1 << (baseZoom - zoom)); + TileCoordinate y = baseY / (1 << (baseZoom - zoom)); - if (dstIndex.x == x && dstIndex.y == y) { - if (object.second.oo.minZoom <= zoom) { - output.push_back(outputObjectWithId(object.second)); + if (dstIndex.x == x && dstIndex.y == y) { + if (objects[i][j].oo.minZoom <= zoom) { + output.push_back(outputObjectWithId(objects[i][j])); + } } } } @@ -218,98 +218,81 @@ template void collectObjectsForTileTemplate( const TileCoordinates& dstIndex, std::vector& output ) { + if (zoom < CLUSTER_ZOOM) + throw std::runtime_error("collectObjectsForTileTemplate should not be called for low zooms"); + uint16_t z6OffsetDivisor = baseZoom >= CLUSTER_ZOOM ? (1 << (baseZoom - CLUSTER_ZOOM)) : 1; for (size_t i = iStart; i < iEnd; i++) { - if (zoom >= CLUSTER_ZOOM) { - // If z >= 6, we can compute the exact bounds within the objects array. - // Translate to the base zoom, then do a binary search to find - // the starting point. - TileCoordinate z6x = dstIndex.x / (1 << (zoom - CLUSTER_ZOOM)); - TileCoordinate z6y = dstIndex.y / (1 << (zoom - CLUSTER_ZOOM)); - - TileCoordinate baseX = dstIndex.x * (1 << (baseZoom - zoom)); - TileCoordinate baseY = dstIndex.y * (1 << (baseZoom - zoom)); - - Z6Offset needleX = baseX - z6x * z6OffsetDivisor; - Z6Offset needleY = baseY - z6y * z6OffsetDivisor; - - // Kind of gross that we have to do this. Might be better if we split - // into two arrays, one of x/y and one of OOs. Would have better locality for - // searching, too. - OutputObject dummyOo(POINT_, 0, 0, 0, 0); - const size_t bz = baseZoom; - - const OO targetXY = {dummyOo, needleX, needleY }; - auto iter = std::lower_bound( - objects[i].begin(), - objects[i].end(), - targetXY, - [bz](const OO& a, const OO& b) { - // Cluster by parent zoom, so that a subsequent search - // can find a contiguous range of entries for any tile - // at zoom 6 or higher. - const size_t aX = a.x; - const size_t aY = a.y; - const size_t bX = b.x; - const size_t bY = b.y; - for (size_t z = CLUSTER_ZOOM; z <= bz; z++) { - const auto aXz = aX / (1 << (bz - z)); - const auto aYz = aY / (1 << (bz - z)); - const auto bXz = bX / (1 << (bz - z)); - const auto bYz = bY / (1 << (bz - z)); - - if (aXz != bXz) - return aXz < bXz; - - if (aYz != bYz) - return aYz < bYz; - } - return false; - } - ); - for (; iter != objects[i].end(); iter++) { - // Compute the x, y at the base zoom level - TileCoordinate baseX = z6x * z6OffsetDivisor + iter->x; - TileCoordinate baseY = z6y * z6OffsetDivisor + iter->y; - - // Translate the x, y at the requested zoom level - TileCoordinate x = baseX / (1 << (baseZoom - zoom)); - TileCoordinate y = baseY / (1 << (baseZoom - zoom)); - - if (dstIndex.x == x && dstIndex.y == y) { - if (iter->oo.minZoom <= zoom) { - output.push_back(outputObjectWithId(*iter)); - } - } else { - // Short-circuit when we're confident we'd no longer see relevant matches. - // We've ordered the entries in `objects` such that all objects that - // share the same tile at any zoom are in contiguous runs. - // - // Thus, as soon as we fail to find a match, we can stop looking. - break; - } + // If z >= 6, we can compute the exact bounds within the objects array. + // Translate to the base zoom, then do a binary search to find + // the starting point. + TileCoordinate z6x = dstIndex.x / (1 << (zoom - CLUSTER_ZOOM)); + TileCoordinate z6y = dstIndex.y / (1 << (zoom - CLUSTER_ZOOM)); + + TileCoordinate baseX = dstIndex.x * (1 << (baseZoom - zoom)); + TileCoordinate baseY = dstIndex.y * (1 << (baseZoom - zoom)); + + Z6Offset needleX = baseX - z6x * z6OffsetDivisor; + Z6Offset needleY = baseY - z6y * z6OffsetDivisor; + + // Kind of gross that we have to do this. Might be better if we split + // into two arrays, one of x/y and one of OOs. Would have better locality for + // searching, too. + OutputObject dummyOo(POINT_, 0, 0, 0, 0); + const size_t bz = baseZoom; + + const OO targetXY = {dummyOo, needleX, needleY }; + auto iter = std::lower_bound( + objects[i].begin(), + objects[i].end(), + targetXY, + [bz](const OO& a, const OO& b) { + // Cluster by parent zoom, so that a subsequent search + // can find a contiguous range of entries for any tile + // at zoom 6 or higher. + const size_t aX = a.x; + const size_t aY = a.y; + const size_t bX = b.x; + const size_t bY = b.y; + for (size_t z = CLUSTER_ZOOM; z <= bz; z++) { + const auto aXz = aX / (1 << (bz - z)); + const auto aYz = aY / (1 << (bz - z)); + const auto bXz = bX / (1 << (bz - z)); + const auto bYz = bY / (1 << (bz - z)); + + if (aXz != bXz) + return aXz < bXz; + if (aYz != bYz) + return aYz < bYz; + } + return false; } - } else { - const size_t z6x = i / CLUSTER_ZOOM_WIDTH; - const size_t z6y = i % CLUSTER_ZOOM_WIDTH; - - for (size_t j = 0; j < objects[i].size(); j++) { - // Compute the x, y at the base zoom level - TileCoordinate baseX = z6x * z6OffsetDivisor + objects[i][j].x; - TileCoordinate baseY = z6y * z6OffsetDivisor + objects[i][j].y; - - // Translate the x, y at the requested zoom level - TileCoordinate x = baseX / (1 << (baseZoom - zoom)); - TileCoordinate y = baseY / (1 << (baseZoom - zoom)); - - if (dstIndex.x == x && dstIndex.y == y) { - if (objects[i][j].oo.minZoom <= zoom) { - output.push_back(outputObjectWithId(objects[i][j])); - } + ); + + for (; iter != objects[i].end(); iter++) { + // Compute the x, y at the base zoom level + TileCoordinate baseX = z6x * z6OffsetDivisor + iter->x; + TileCoordinate baseY = z6y * z6OffsetDivisor + iter->y; + + // Translate the x, y at the requested zoom level + TileCoordinate x = baseX / (1 << (baseZoom - zoom)); + TileCoordinate y = baseY / (1 << (baseZoom - zoom)); + + if (dstIndex.x == x && dstIndex.y == y) { + if (iter->oo.minZoom <= zoom) { + output.push_back(outputObjectWithId(*iter)); } + } else { + // Short-circuit when we're confident we'd no longer see relevant matches. + // We've ordered the entries in `objects` such that all objects that + // share the same tile at any zoom are in contiguous runs. + // + // Thus, as soon as we fail to find a match, we can stop looking. + break; } + } } } @@ -355,9 +338,9 @@ class TileDataSource { // If config.include_ids is true, objectsWithIds will be populated. // Otherwise, objects. std::vector> objects; - AppendVectorNS::AppendVector> lowZoomObjects; + std::vector> lowZoomObjects; std::vector> objectsWithIds; - AppendVectorNS::AppendVector> lowZoomObjectsWithIds; + std::vector> lowZoomObjectsWithIds; // rtree index of large objects using oo_rtree_param_type = boost::geometry::index::quadratic<128>; diff --git a/src/tile_data.cpp b/src/tile_data.cpp index b56e97e7..fbae2038 100644 --- a/src/tile_data.cpp +++ b/src/tile_data.cpp @@ -47,7 +47,9 @@ TileDataSource::TileDataSource(size_t threadNum, unsigned int baseZoom, bool inc z6OffsetDivisor(baseZoom >= CLUSTER_ZOOM ? (1 << (baseZoom - CLUSTER_ZOOM)) : 1), objectsMutex(threadNum * 4), objects(CLUSTER_ZOOM_AREA), + lowZoomObjects(CLUSTER_ZOOM_AREA), objectsWithIds(CLUSTER_ZOOM_AREA), + lowZoomObjectsWithIds(CLUSTER_ZOOM_AREA), baseZoom(baseZoom), pointStores(threadNum), linestringStores(threadNum), @@ -149,8 +151,6 @@ void TileDataSource::collectObjectsForTile( size_t iStart = 0; size_t iEnd = objects.size(); - // TODO: we could also narrow the search space for z1..z5, too. - // They're less important, as they have fewer tiles. if (zoom >= CLUSTER_ZOOM) { // Compute the x, y at the base zoom level TileCoordinate z6x = dstIndex.x / (1 << (zoom - CLUSTER_ZOOM)); From 00bb73b5a98b9efde003d57b74ebb29278174d00 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 13:16:43 -0500 Subject: [PATCH 32/81] implement SortedNodeStore::contains --- include/sorted_node_store.h | 2 +- src/sorted_node_store.cpp | 40 +++++++++++++++++++++++++++++++++ test/sorted_node_store.test.cpp | 34 +++++++++++++++++----------- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/include/sorted_node_store.h b/include/sorted_node_store.h index 8f276f4a..0d12c460 100644 --- a/include/sorted_node_store.h +++ b/include/sorted_node_store.h @@ -70,7 +70,7 @@ class SortedNodeStore : public NodeStore reopen(); } - bool contains(size_t shard, NodeID ID) const override { throw std::runtime_error("SortedNodeStore::contains not implemented"); } + bool contains(size_t shard, NodeID id) const override; size_t shard() const override { return 0; } size_t shards() const override { return 1; } diff --git a/src/sorted_node_store.cpp b/src/sorted_node_store.cpp index 6761ad00..f99baa58 100644 --- a/src/sorted_node_store.cpp +++ b/src/sorted_node_store.cpp @@ -91,6 +91,46 @@ SortedNodeStore::~SortedNodeStore() { s(this) = ThreadStorage(); } +bool SortedNodeStore::contains(size_t shard, NodeID id) const { + const size_t groupIndex = id / (GroupSize * ChunkSize); + const size_t chunk = (id % (GroupSize * ChunkSize)) / ChunkSize; + const uint64_t chunkMaskByte = chunk / 8; + const uint64_t chunkMaskBit = chunk % 8; + + const uint64_t nodeMaskByte = (id % ChunkSize) / 8; + const uint64_t nodeMaskBit = id % 8; + + GroupInfo* groupPtr = groups[groupIndex]; + + if (groupPtr == nullptr) + return false; + + size_t chunkOffset = 0; + { + chunkOffset = popcnt(groupPtr->chunkMask, chunkMaskByte); + uint8_t maskByte = groupPtr->chunkMask[chunkMaskByte]; + maskByte = maskByte & ((1 << chunkMaskBit) - 1); + chunkOffset += popcnt(&maskByte, 1); + + if (!(groupPtr->chunkMask[chunkMaskByte] & (1 << chunkMaskBit))) + return false; + } + + uint16_t scaledOffset = groupPtr->chunkOffsets[chunkOffset]; + ChunkInfoBase* basePtr = (ChunkInfoBase*)(((char *)(groupPtr->chunkOffsets + popcnt(groupPtr->chunkMask, 32))) + (scaledOffset * ChunkAlignment)); + + size_t nodeOffset = 0; + nodeOffset = popcnt(basePtr->nodeMask, nodeMaskByte); + uint8_t maskByte = basePtr->nodeMask[nodeMaskByte]; + maskByte = maskByte & ((1 << nodeMaskBit) - 1); + nodeOffset += popcnt(&maskByte, 1); + if (!(basePtr->nodeMask[nodeMaskByte] & (1 << nodeMaskBit))) + return false; + + + return true; +} + LatpLon SortedNodeStore::at(const NodeID id) const { const size_t groupIndex = id / (GroupSize * ChunkSize); const size_t chunk = (id % (GroupSize * ChunkSize)) / ChunkSize; diff --git a/test/sorted_node_store.test.cpp b/test/sorted_node_store.test.cpp index ba7edb2d..de66445f 100644 --- a/test/sorted_node_store.test.cpp +++ b/test/sorted_node_store.test.cpp @@ -3,23 +3,31 @@ #include "sorted_node_store.h" MU_TEST(test_sorted_node_store) { - SortedNodeStore s1(true), s2(true); - mu_check(s1.size() == 0); - mu_check(s2.size() == 0); + bool compressed = true; - s1.batchStart(); - s2.batchStart(); + for (int i = 0; i < 2; i++) { + compressed = !compressed; + SortedNodeStore s1(compressed), s2(compressed); + mu_check(s1.size() == 0); + mu_check(s2.size() == 0); - s1.insert({ {1, {2, 3 } } }); - s2.insert({ {2, {3, 4 } } }); + s1.batchStart(); + s2.batchStart(); - s1.finalize(1); - s2.finalize(1); + s1.insert({ {1, {2, 3 } } }); + s2.insert({ {2, {3, 4 } } }); - mu_check(s1.size() == 1); - mu_check(s1.at(1) == LatpLon({2, 3})); - mu_check(s2.size() == 1); - mu_check(s2.at(2) == LatpLon({3, 4})); + s1.finalize(1); + s2.finalize(1); + + mu_check(s1.size() == 1); + mu_check(s1.at(1) == LatpLon({2, 3})); + mu_check(s1.contains(0, 1)); + mu_check(!s1.contains(0, 2)); + mu_check(!s1.contains(0, 1ull << 34)); + mu_check(s2.size() == 1); + mu_check(s2.at(2) == LatpLon({3, 4})); + } } MU_TEST_SUITE(test_suite_sorted_node_store) { From e8be59ca998b6557061927365271cb5c9e5ac7b4 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 13:23:32 -0500 Subject: [PATCH 33/81] implement SortedWayStore::contains --- include/sorted_way_store.h | 2 +- src/sorted_way_store.cpp | 50 ++++++++++++++++++++++++++++++++++ test/sorted_way_store.test.cpp | 37 ++++++++++++++++++------- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/include/sorted_way_store.h b/include/sorted_way_store.h index b28c4257..23271deb 100644 --- a/include/sorted_way_store.h +++ b/include/sorted_way_store.h @@ -95,7 +95,7 @@ class SortedWayStore: public WayStore { std::size_t size() const override; void finalize(unsigned int threadNum) override; - bool contains(size_t shard, WayID id) const override { throw std::runtime_error("SortedWayStore::contains not implemented"); } + bool contains(size_t shard, WayID id) const override; size_t shard() const override { return 0; } size_t shards() const override { return 1; } diff --git a/src/sorted_way_store.cpp b/src/sorted_way_store.cpp index c1dd9ae2..669d01a2 100644 --- a/src/sorted_way_store.cpp +++ b/src/sorted_way_store.cpp @@ -90,6 +90,56 @@ void SortedWayStore::reopen() { } +bool SortedWayStore::contains(size_t shard, WayID id) const { + const size_t groupIndex = id / (GroupSize * ChunkSize); + const size_t chunk = (id % (GroupSize * ChunkSize)) / ChunkSize; + const uint64_t chunkMaskByte = chunk / 8; + const uint64_t chunkMaskBit = chunk % 8; + + const uint64_t wayMaskByte = (id % ChunkSize) / 8; + const uint64_t wayMaskBit = id % 8; + + GroupInfo* groupPtr = groups[groupIndex]; + + if (groupPtr == nullptr) + return false; + + size_t chunkOffset = 0; + { + chunkOffset = popcnt(groupPtr->chunkMask, chunkMaskByte); + uint8_t maskByte = groupPtr->chunkMask[chunkMaskByte]; + maskByte = maskByte & ((1 << chunkMaskBit) - 1); + chunkOffset += popcnt(&maskByte, 1); + + if (!(groupPtr->chunkMask[chunkMaskByte] & (1 << chunkMaskBit))) + return false; + } + + ChunkInfo* chunkPtr = (ChunkInfo*)((char*)groupPtr + groupPtr->chunkOffsets[chunkOffset]); + const size_t numWays = popcnt(chunkPtr->smallWayMask, 32) + popcnt(chunkPtr->bigWayMask, 32); + + { + size_t wayOffset = 0; + wayOffset = popcnt(chunkPtr->smallWayMask, wayMaskByte); + uint8_t maskByte = chunkPtr->smallWayMask[wayMaskByte]; + maskByte = maskByte & ((1 << wayMaskBit) - 1); + wayOffset += popcnt(&maskByte, 1); + if (chunkPtr->smallWayMask[wayMaskByte] & (1 << wayMaskBit)) + return true; + } + + size_t wayOffset = 0; + wayOffset += popcnt(chunkPtr->smallWayMask, 32); + wayOffset += popcnt(chunkPtr->bigWayMask, wayMaskByte); + uint8_t maskByte = chunkPtr->bigWayMask[wayMaskByte]; + maskByte = maskByte & ((1 << wayMaskBit) - 1); + wayOffset += popcnt(&maskByte, 1); + if (!(chunkPtr->bigWayMask[wayMaskByte] & (1 << wayMaskBit))) + return false; + + return true; +} + std::vector SortedWayStore::at(WayID id) const { const size_t groupIndex = id / (GroupSize * ChunkSize); const size_t chunk = (id % (GroupSize * ChunkSize)) / ChunkSize; diff --git a/test/sorted_way_store.test.cpp b/test/sorted_way_store.test.cpp index 8c4c432d..65d34816 100644 --- a/test/sorted_way_store.test.cpp +++ b/test/sorted_way_store.test.cpp @@ -75,19 +75,36 @@ MU_TEST(test_encode_way) { } MU_TEST(test_multiple_stores) { - TestNodeStore ns; - SortedWayStore s1(true, ns), s2(true, ns); - s1.batchStart(); - s2.batchStart(); + bool compressed = false; + + for (int i = 0; i < 2; i++) { + compressed = !compressed; + TestNodeStore ns; + SortedWayStore s1(compressed, ns), s2(compressed, ns); + s1.batchStart(); + s2.batchStart(); + + s1.insertNodes({{ 1, { 1 } }}); - s1.insertNodes({{ 1, { 1 } }}); - s2.insertNodes({{ 2, { 2 } }}); + // We store small ways differently than large ways, so + // store both kinds for testing. + std::vector longWay; + for (int i = 200; i < 2048; i++) + longWay.push_back(i + 3 * (i % 37)); - s1.finalize(1); - s2.finalize(1); + s1.insertNodes({{ 42, longWay }}); + s2.insertNodes({{ 2, { 2 } }}); - mu_check(s1.size() == 1); - mu_check(s2.size() == 1); + s1.finalize(1); + s2.finalize(1); + + mu_check(s1.size() == 2); + mu_check(s2.size() == 1); + + mu_check(s1.contains(0, 1)); + mu_check(s1.contains(0, 42)); + mu_check(!s1.contains(0, 2)); + } } MU_TEST(test_way_store) { From 792d1b367c489a5ee785aabf4656c9085d2a17a2 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 18:27:32 -0500 Subject: [PATCH 34/81] use TileCoordinatesSet --- src/tilemaker.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index 8a3f6419..d30d556f 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -492,7 +492,7 @@ int main(int argc, char* argv[]) { // The clipping bbox check is expensive - as an optimization, compute the set of // z6 tiles that are wholly covered by the clipping box. Membership in this // set is quick to test. - std::set coveredZ6Tiles; + TileCoordinatesSet coveredZ6Tiles(6); if (hasClippingBox) { for (int x = 0; x < 1 << 6; x++) { for (int y = 0; y < 1 << 6; y++) { @@ -500,7 +500,7 @@ int main(int argc, char* argv[]) { TileBbox(TileCoordinates(x, y), 6, false, false).getTileBox(), clippingBox )) - coveredZ6Tiles.insert(TileCoordinates(x, y)); + coveredZ6Tiles.set(x, y); } } } @@ -533,7 +533,7 @@ int main(int argc, char* argv[]) { if (zoom >= 6) { TileCoordinate z6x = x / (1 << (zoom - 6)); TileCoordinate z6y = y / (1 << (zoom - 6)); - isInAWhollyCoveredZ6Tile = coveredZ6Tiles.find(TileCoordinates(z6x, z6y)) != coveredZ6Tiles.end(); + isInAWhollyCoveredZ6Tile = coveredZ6Tiles.test(z6x, z6y); } if(!isInAWhollyCoveredZ6Tile && !boost::geometry::intersects(TileBbox(TileCoordinates(x, y), zoom, false, false).getTileBox(), clippingBox)) From 2df30816759c35c714608cbe7a4a52b15e64dfed Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 18:57:51 -0500 Subject: [PATCH 35/81] faster covered tile enumeration Do a single pass, rather than one pass per zoom. --- include/tile_data.h | 23 ++++++++++++++--------- src/tile_data.cpp | 39 +++++++++++++++++++++------------------ src/tilemaker.cpp | 24 ++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/include/tile_data.h b/include/tile_data.h index 804761c8..2c5fe4df 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -137,9 +137,9 @@ template void collectTilesWithObjectsAtZoomTemplate( const unsigned int& baseZoom, const typename std::vector>::iterator objects, const size_t size, - const unsigned int zoom, - TileCoordinatesSet& output + std::vector& zooms ) { + size_t maxZoom = zooms.size() - 1; uint16_t z6OffsetDivisor = baseZoom >= CLUSTER_ZOOM ? (1 << (baseZoom - CLUSTER_ZOOM)) : 1; int64_t lastX = -1; int64_t lastY = -1; @@ -153,13 +153,18 @@ template void collectTilesWithObjectsAtZoomTemplate( TileCoordinate baseY = z6y * z6OffsetDivisor + objects[i][j].y; // Translate the x, y at the requested zoom level - TileCoordinate x = baseX / (1 << (baseZoom - zoom)); - TileCoordinate y = baseY / (1 << (baseZoom - zoom)); + TileCoordinate x = baseX / (1 << (baseZoom - maxZoom)); + TileCoordinate y = baseY / (1 << (baseZoom - maxZoom)); if (lastX != x || lastY != y) { - output.set(x, y); lastX = x; lastY = y; + + for (int zoom = maxZoom; zoom >= 0; zoom--) { + zooms[zoom].set(x, y); + x /= 2; + y /= 2; + } } } } @@ -360,9 +365,9 @@ class TileDataSource { public: TileDataSource(size_t threadNum, unsigned int baseZoom, bool includeID); - void collectTilesWithObjectsAtZoom(uint zoom, TileCoordinatesSet& output); + void collectTilesWithObjectsAtZoom(std::vector& zooms); - void collectTilesWithLargeObjectsAtZoom(uint zoom, TileCoordinatesSet& output); + void collectTilesWithLargeObjectsAtZoom(std::vector& zooms); void collectObjectsForTile(uint zoom, TileCoordinates dstIndex, std::vector& output); void finalize(size_t threadNum); @@ -473,9 +478,9 @@ class TileDataSource { } }; -TileCoordinatesSet getTilesAtZoom( +void populateTilesAtZoom( const std::vector& sources, - unsigned int zoom + std::vector& zooms ); #endif //_TILE_DATA_H diff --git a/src/tile_data.cpp b/src/tile_data.cpp index fbae2038..8a8053bf 100644 --- a/src/tile_data.cpp +++ b/src/tile_data.cpp @@ -53,8 +53,8 @@ TileDataSource::TileDataSource(size_t threadNum, unsigned int baseZoom, bool inc baseZoom(baseZoom), pointStores(threadNum), linestringStores(threadNum), - multipolygonStores(threadNum), multilinestringStores(threadNum), + multipolygonStores(threadNum), multiPolygonClipCache(ClipCache(threadNum, baseZoom)), multiLinestringClipCache(ClipCache(threadNum, baseZoom)) { @@ -108,32 +108,39 @@ void TileDataSource::addObjectToSmallIndex(const TileCoordinates& index, const O }); } -void TileDataSource::collectTilesWithObjectsAtZoom(uint zoom, TileCoordinatesSet& output) { +void TileDataSource::collectTilesWithObjectsAtZoom(std::vector& zooms) { // Scan through all shards. Convert to base zoom, then convert to the requested zoom. - collectTilesWithObjectsAtZoomTemplate(baseZoom, objects.begin(), objects.size(), zoom, output); - collectTilesWithObjectsAtZoomTemplate(baseZoom, objectsWithIds.begin(), objectsWithIds.size(), zoom, output); + collectTilesWithObjectsAtZoomTemplate(baseZoom, objects.begin(), objects.size(), zooms); + collectTilesWithObjectsAtZoomTemplate(baseZoom, objectsWithIds.begin(), objectsWithIds.size(), zooms); } -void addCoveredTilesToOutput(const uint baseZoom, const uint zoom, const Box& box, TileCoordinatesSet& output) { - int scale = pow(2, baseZoom-zoom); +void addCoveredTilesToOutput(const uint baseZoom, std::vector& zooms, const Box& box) { + size_t maxZoom = zooms.size() - 1; + int scale = pow(2, baseZoom - maxZoom); TileCoordinate minx = box.min_corner().x() / scale; TileCoordinate maxx = box.max_corner().x() / scale; TileCoordinate miny = box.min_corner().y() / scale; TileCoordinate maxy = box.max_corner().y() / scale; for (int x=minx; x<=maxx; x++) { for (int y=miny; y<=maxy; y++) { - output.set(x, y); + size_t zx = x, zy = y; + + for (int zoom = maxZoom; zoom >= 0; zoom--) { + zooms[zoom].set(zx, zy); + zx /= 2; + zy /= 2; + } } } } // Find the tiles used by the "large objects" from the rtree index -void TileDataSource::collectTilesWithLargeObjectsAtZoom(uint zoom, TileCoordinatesSet &output) { +void TileDataSource::collectTilesWithLargeObjectsAtZoom(std::vector& zooms) { for(auto const &result: boxRtree) - addCoveredTilesToOutput(baseZoom, zoom, result.first, output); + addCoveredTilesToOutput(baseZoom, zooms, result.first); for(auto const &result: boxRtreeWithIds) - addCoveredTilesToOutput(baseZoom, zoom, result.first, output); + addCoveredTilesToOutput(baseZoom, zooms, result.first); } // Copy objects from the tile at dstIndex (in the dataset srcTiles) into output @@ -369,18 +376,14 @@ void TileDataSource::reportSize() const { std::cout << "Generated points: " << (points - 1) << ", lines: " << (linestrings - 2) << ", polygons: " << (polygons - 1) << std::endl; } -TileCoordinatesSet getTilesAtZoom( +void populateTilesAtZoom( const std::vector& sources, - unsigned int zoom + std::vector& zooms ) { - TileCoordinatesSet tileCoordinates(zoom); - for(size_t i=0; icollectTilesWithObjectsAtZoom(zoom, tileCoordinates); - sources[i]->collectTilesWithLargeObjectsAtZoom(zoom, tileCoordinates); + sources[i]->collectTilesWithObjectsAtZoom(zooms); + sources[i]->collectTilesWithLargeObjectsAtZoom(zooms); } - - return tileCoordinates; } std::vector TileDataSource::getObjectsForTile( diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index d30d556f..6ebfef0d 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -506,7 +506,26 @@ int main(int argc, char* argv[]) { } std::deque> tileCoordinates; - std::cout << "collecting tiles:"; + std::vector zoomResults; + for (uint zoom = 0; zoom <= sharedData.config.endZoom; zoom++) { + zoomResults.push_back(TileCoordinatesSet(zoom)); + } + + { +#ifdef CLOCK_MONOTONIC + timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); +#endif + std::cout << "collecting tiles" << std::flush; + populateTilesAtZoom(sources, zoomResults); +#ifdef CLOCK_MONOTONIC + clock_gettime(CLOCK_MONOTONIC, &end); + uint64_t tileNs = 1e9 * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; + std::cout << ": " << (uint32_t)(tileNs / 1e6) << "ms"; +#endif + } + + std::cout << ", filtering tiles:" << std::flush; for (uint zoom=sharedData.config.startZoom; zoom <= sharedData.config.endZoom; zoom++) { std::cout << " z" << std::to_string(zoom) << std::flush; #ifdef CLOCK_MONOTONIC @@ -514,7 +533,7 @@ int main(int argc, char* argv[]) { clock_gettime(CLOCK_MONOTONIC, &start); #endif - auto zoomResult = getTilesAtZoom(sources, zoom); + const auto& zoomResult = zoomResults[zoom]; int numTiles = 0; for (int x = 0; x < 1 << zoom; x++) { for (int y = 0; y < 1 << zoom; y++) { @@ -554,6 +573,7 @@ int main(int argc, char* argv[]) { #endif std::cout << ")" << std::flush; } + zoomResults.clear(); std::cout << std::endl; From b9434f2c65ac26dc60b332cf8ccdebb724550262 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 21:28:25 -0500 Subject: [PATCH 36/81] add ShardedNodeStore This distributes nodes into one of 8 shards, trying to roughly group parts of the globe by complexity. This should help with locality when writing tiles. A future commit will add a ShardedWayStore and teach read_pbf to read in a locality-aware manner, which should help when reading ways. --- CMakeLists.txt | 1 + Makefile | 1 + include/node_store.h | 1 - include/node_stores.h | 3 +- include/sharded_node_store.h | 30 +++++++++++++ include/sorted_node_store.h | 1 - src/sharded_node_store.cpp | 86 ++++++++++++++++++++++++++++++++++++ src/sorted_node_store.cpp | 3 +- src/sorted_way_store.cpp | 2 +- src/tilemaker.cpp | 45 +++++++++++++------ 10 files changed, 153 insertions(+), 20 deletions(-) create mode 100644 include/sharded_node_store.h create mode 100644 src/sharded_node_store.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d69e61ed..3c301534 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,7 @@ file(GLOB tilemaker_src_files src/pooled_string.cpp src/read_pbf.cpp src/read_shp.cpp + src/sharded_node_store.cpp src/shared_data.cpp src/shp_mem_tiles.cpp src/sorted_node_store.cpp diff --git a/Makefile b/Makefile index 9eae44c3..0a63416f 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,7 @@ tilemaker: \ src/pooled_string.o \ src/read_pbf.o \ src/read_shp.o \ + src/sharded_node_store.o \ src/shared_data.o \ src/shp_mem_tiles.o \ src/sorted_node_store.o \ diff --git a/include/node_store.h b/include/node_store.h index 9ef2a4c6..a2547fd5 100644 --- a/include/node_store.h +++ b/include/node_store.h @@ -25,7 +25,6 @@ class NodeStore virtual LatpLon at(NodeID i) const = 0; virtual bool contains(size_t shard, NodeID id) const = 0; - virtual size_t shard() const = 0; virtual size_t shards() const = 0; }; diff --git a/include/node_stores.h b/include/node_stores.h index f093081f..80a94868 100644 --- a/include/node_stores.h +++ b/include/node_stores.h @@ -5,6 +5,7 @@ #include #include "node_store.h" #include "sorted_node_store.h" +#include "sharded_node_store.h" #include "mmap_allocator.h" class BinarySearchNodeStore : public NodeStore @@ -25,7 +26,6 @@ class BinarySearchNodeStore : public NodeStore void batchStart() {} bool contains(size_t shard, NodeID id) const override; - size_t shard() const override { return 0; } size_t shards() const override { return 1; } @@ -59,7 +59,6 @@ class CompactNodeStore : public NodeStore // CompactNodeStore has no metadata to know whether or not it contains // a node, so it's not suitable for used in sharded scenarios. bool contains(size_t shard, NodeID id) const override { return true; } - size_t shard() const override { return 0; } size_t shards() const override { return 1; } private: diff --git a/include/sharded_node_store.h b/include/sharded_node_store.h new file mode 100644 index 00000000..44938126 --- /dev/null +++ b/include/sharded_node_store.h @@ -0,0 +1,30 @@ +#ifndef _SHARDED_NODE_STORE +#define _SHARDED_NODE_STORE + +#include +#include +#include "node_store.h" + +class ShardedNodeStore : public NodeStore { +public: + ShardedNodeStore(std::function()> createNodeStore); + ~ShardedNodeStore(); + void reopen() override; + void finalize(size_t threadNum) override; + LatpLon at(NodeID i) const override; + size_t size() const override; + void batchStart() override; + void insert(const std::vector& elements) override; + void clear() { + reopen(); + } + + bool contains(size_t shard, NodeID id) const override; + size_t shards() const override; + +private: + std::function()> createNodeStore; + std::vector> stores; +}; + +#endif diff --git a/include/sorted_node_store.h b/include/sorted_node_store.h index 0d12c460..0e8d2e24 100644 --- a/include/sorted_node_store.h +++ b/include/sorted_node_store.h @@ -71,7 +71,6 @@ class SortedNodeStore : public NodeStore } bool contains(size_t shard, NodeID id) const override; - size_t shard() const override { return 0; } size_t shards() const override { return 1; } private: diff --git a/src/sharded_node_store.cpp b/src/sharded_node_store.cpp new file mode 100644 index 00000000..92169986 --- /dev/null +++ b/src/sharded_node_store.cpp @@ -0,0 +1,86 @@ +#include "sharded_node_store.h" + +ShardedNodeStore::ShardedNodeStore(std::function()> createNodeStore): + createNodeStore(createNodeStore) { + for (int i = 0; i < shards(); i++) + stores.push_back(createNodeStore()); +} + +ShardedNodeStore::~ShardedNodeStore() { +} + +void ShardedNodeStore::reopen() { + for (auto& store : stores) + store->reopen(); +} + +void ShardedNodeStore::finalize(size_t threadNum) { + for (auto& store : stores) + store->finalize(threadNum); +} + +LatpLon ShardedNodeStore::at(NodeID id) const { + // TODO: look in the last store we successfully found something, using + // a thread local + for (int i = 0; i < shards(); i++) + if (stores[i]->contains(0, id) || i == shards() - 1) + return stores[i]->at(id); +} + +size_t ShardedNodeStore::size() const { + size_t rv = 0; + for (auto& store : stores) + rv += store->size(); + + return rv; +} + +void ShardedNodeStore::batchStart() { + for (auto& store : stores) + store->batchStart(); +} + +size_t pickStore(const LatpLon& el) { + // Assign the element to a store. This is pretty naive, we could likely do better-- + // Europe still basically gets its own bucket, but probably should be split up + // more. + + const size_t z3x = lon2tilex(el.lon / 10000000, 3); + const size_t z3y = latp2tiley(el.latp / 10000000, 3); + + if (z3x == 4 && z3y == 2) return 4; // Central Europe + if (z3x == 5 && z3y == 2) return 5; // Western Russia + if (z3x == 4 && z3y == 3) return 6; // North Africa + if (z3x == 5 && z3y == 3) return 7; // India + + const size_t z2x = z3x / 2; + const size_t z2y = z3y / 2; + + if (z2x == 3 && z2y == 1) return 3; // Asia, Russia + if (z2x == 1 && z2y == 1) return 2; // North Atlantic Ocean and bordering countries + if (z2x == 0 && z2y == 1) return 1; // North America + +// std::cout << "z2x=" << std::to_string(z2x) << ", z2y=" << std::to_string(z2y) << std::endl; + return 0; // Artic, Antartcica, Oceania +} + +void ShardedNodeStore::insert(const std::vector& elements) { + std::vector> perStore(shards()); + + for (const auto& el : elements) { + perStore[pickStore(el.second)].push_back(el); + } + + for (int i = 0; i < shards(); i++) { + if (!perStore[i].empty()) + stores[i]->insert(perStore[i]); + } +} + +bool ShardedNodeStore::contains(size_t shard, NodeID id) const { + return stores[shard]->contains(0, id); +} + +size_t ShardedNodeStore::shards() const { + return 8; +} diff --git a/src/sorted_node_store.cpp b/src/sorted_node_store.cpp index f99baa58..174664c3 100644 --- a/src/sorted_node_store.cpp +++ b/src/sorted_node_store.cpp @@ -58,6 +58,7 @@ namespace SortedNodeStoreTypes { using namespace SortedNodeStoreTypes; SortedNodeStore::SortedNodeStore(bool compressNodes): compressNodes(compressNodes) { + s(this); // allocate our ThreadStorage before multi-threading reopen(); } @@ -320,7 +321,7 @@ void SortedNodeStore::finalize(size_t threadNum) { orphanage.clear(); - std::cout << "SortedNodeStore: " << totalGroups << " groups, " << totalChunks << " chunks, " << totalNodes.load() << " nodes, " << totalGroupSpace.load() << " bytes (" << (1000ull * (totalAllocatedSpace.load() - totalGroupSpace.load()) / totalAllocatedSpace.load()) / 10.0 << "% wasted)" << std::endl; + std::cout << "SortedNodeStore: " << totalGroups << " groups, " << totalChunks << " chunks, " << totalNodes.load() << " nodes, " << totalGroupSpace.load() << " bytes (" << (1000ull * (totalAllocatedSpace.load() - totalGroupSpace.load()) / (totalAllocatedSpace.load() + 1)) / 10.0 << "% wasted)" << std::endl; /* for (int i = 0; i < 257; i++) std::cout << "chunkSizeFreqs[ " << i << " ]= " << chunkSizeFreqs[i].load() << std::endl; diff --git a/src/sorted_way_store.cpp b/src/sorted_way_store.cpp index 669d01a2..e7ff4841 100644 --- a/src/sorted_way_store.cpp +++ b/src/sorted_way_store.cpp @@ -59,6 +59,7 @@ namespace SortedWayStoreTypes { using namespace SortedWayStoreTypes; SortedWayStore::SortedWayStore(bool compressWays, const NodeStore& nodeStore): compressWays(compressWays), nodeStore(nodeStore) { + s(this); // allocate our ThreadStorage before multi-threading reopen(); } @@ -116,7 +117,6 @@ bool SortedWayStore::contains(size_t shard, WayID id) const { } ChunkInfo* chunkPtr = (ChunkInfo*)((char*)groupPtr + groupPtr->chunkOffsets[chunkOffset]); - const size_t numWays = popcnt(chunkPtr->smallWayMask, 32) + popcnt(chunkPtr->bigWayMask, 32); { size_t wayOffset = 0; diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index 6ebfef0d..3a32168a 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -285,8 +285,6 @@ int main(int argc, char* argv[]) { } // For each tile, objects to be used in processing - shared_ptr nodeStore; - bool allPbfsHaveSortTypeThenID = true; bool anyPbfHasLocationsOnWays = false; @@ -297,22 +295,41 @@ int main(int argc, char* argv[]) { } } - if (osmStoreCompact) - nodeStore = make_shared(); - else { - if (allPbfsHaveSortTypeThenID) - nodeStore = make_shared(!osmStoreUncompressedNodes); - else - nodeStore = make_shared(); - } + auto createNodeStore = [allPbfsHaveSortTypeThenID, osmStoreCompact, osmStoreUncompressedNodes]() { + if (osmStoreCompact) { + std::shared_ptr rv = make_shared(); + return rv; + } + + if (allPbfsHaveSortTypeThenID) { + std::shared_ptr rv = make_shared(!osmStoreUncompressedNodes); + return rv; + } + std::shared_ptr rv = make_shared(); + return rv; + }; - shared_ptr wayStore; - if (!anyPbfHasLocationsOnWays && allPbfsHaveSortTypeThenID) { - wayStore = make_shared(!osmStoreUncompressedNodes, *nodeStore.get()); + shared_ptr nodeStore; + + // TODO: make this a flag + if (true) { + nodeStore = std::make_shared(createNodeStore); } else { - wayStore = make_shared(); + nodeStore = createNodeStore(); } + auto createWayStore = [anyPbfHasLocationsOnWays, allPbfsHaveSortTypeThenID, osmStoreUncompressedWays, &nodeStore]() { + if (!anyPbfHasLocationsOnWays && allPbfsHaveSortTypeThenID) { + std::shared_ptr rv = make_shared(!osmStoreUncompressedWays, *nodeStore.get()); + return rv; + } + + std::shared_ptr rv = make_shared(); + return rv; + }; + + shared_ptr wayStore = createWayStore(); + OSMStore osmStore(*nodeStore.get(), *wayStore.get()); osmStore.use_compact_store(osmStoreCompact); osmStore.enforce_integrity(!skipIntegrity); From e968b400f73261a42c9f5add61d104342f0e1116 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 17 Dec 2023 23:21:51 -0500 Subject: [PATCH 37/81] add ShardedWayStore Add `--shard-stores` flag. It's not clear yet this'll be a win, will need to benchmark. The cost of reading the PBF blocks repeatedly is a bit higher than I was expecting. It might be worth seeing if we can index the blocks to skip fruitless reads. --- CMakeLists.txt | 1 + Makefile | 1 + include/read_pbf.h | 14 ++- include/sharded_way_store.h | 34 ++++++ include/sorted_way_store.h | 4 +- include/way_store.h | 4 +- include/way_stores.h | 5 +- src/read_pbf.cpp | 204 +++++++++++++++++++++--------------- src/sharded_node_store.cpp | 18 +++- src/sharded_way_store.cpp | 77 ++++++++++++++ src/sorted_way_store.cpp | 2 +- src/tilemaker.cpp | 15 ++- src/way_stores.cpp | 2 +- 13 files changed, 279 insertions(+), 102 deletions(-) create mode 100644 include/sharded_way_store.h create mode 100644 src/sharded_way_store.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c301534..dd3179bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,7 @@ file(GLOB tilemaker_src_files src/read_pbf.cpp src/read_shp.cpp src/sharded_node_store.cpp + src/sharded_way_store.cpp src/shared_data.cpp src/shp_mem_tiles.cpp src/sorted_node_store.cpp diff --git a/Makefile b/Makefile index 0a63416f..81779d79 100644 --- a/Makefile +++ b/Makefile @@ -115,6 +115,7 @@ tilemaker: \ src/read_pbf.o \ src/read_shp.o \ src/sharded_node_store.o \ + src/sharded_way_store.o \ src/shared_data.o \ src/shp_mem_tiles.o \ src/sorted_node_store.o \ diff --git a/include/read_pbf.h b/include/read_pbf.h index b934a563..4ab44612 100644 --- a/include/read_pbf.h +++ b/include/read_pbf.h @@ -53,6 +53,7 @@ class PbfReader using pbfreader_generate_stream = std::function< std::shared_ptr () >; int ReadPbfFile( + uint shards, bool hasSortTypeThenID, const std::unordered_set& nodeKeys, unsigned int threadNum, @@ -79,11 +80,20 @@ class PbfReader const BlockMetadata& blockMetadata, const std::unordered_set& nodeKeys, bool locationsOnWays, - ReadPhase phase + ReadPhase phase, + uint shard, + uint effectiveShard ); bool ReadNodes(OsmLuaProcessing &output, PrimitiveGroup &pg, PrimitiveBlock const &pb, const std::unordered_set &nodeKeyPositions); - bool ReadWays(OsmLuaProcessing &output, PrimitiveGroup &pg, PrimitiveBlock const &pb, bool locationsOnWays); + bool ReadWays( + OsmLuaProcessing &output, + PrimitiveGroup &pg, + PrimitiveBlock const &pb, + bool locationsOnWays, + uint shard, + uint effectiveShards + ); bool ScanRelations(OsmLuaProcessing &output, PrimitiveGroup &pg, PrimitiveBlock const &pb); bool ReadRelations( OsmLuaProcessing& output, diff --git a/include/sharded_way_store.h b/include/sharded_way_store.h new file mode 100644 index 00000000..b57d03e0 --- /dev/null +++ b/include/sharded_way_store.h @@ -0,0 +1,34 @@ +#ifndef _SHARDED_WAY_STORE +#define _SHARDED_WAY_STORE + +#include +#include +#include "way_store.h" + +class NodeStore; + +class ShardedWayStore : public WayStore { +public: + ShardedWayStore(std::function()> createWayStore, const NodeStore& nodeStore); + ~ShardedWayStore(); + void reopen() override; + void batchStart() override; + std::vector at(WayID wayid) const override; + bool requiresNodes() const override; + void insertLatpLons(std::vector &newWays) override; + void insertNodes(const std::vector>>& newWays) override; + void clear() override; + std::size_t size() const override; + void finalize(unsigned int threadNum) override; + + bool contains(size_t shard, WayID id) const override; + WayStore& shard(size_t shard) override; + size_t shards() const override; + +private: + std::function()> createWayStore; + const NodeStore& nodeStore; + std::vector> stores; +}; + +#endif diff --git a/include/sorted_way_store.h b/include/sorted_way_store.h index 23271deb..890a9a53 100644 --- a/include/sorted_way_store.h +++ b/include/sorted_way_store.h @@ -90,13 +90,13 @@ class SortedWayStore: public WayStore { std::vector at(WayID wayid) const override; bool requiresNodes() const override { return true; } void insertLatpLons(std::vector &newWays) override; - const void insertNodes(const std::vector>>& newWays) override; + void insertNodes(const std::vector>>& newWays) override; void clear() override; std::size_t size() const override; void finalize(unsigned int threadNum) override; bool contains(size_t shard, WayID id) const override; - size_t shard() const override { return 0; } + WayStore& shard(size_t shard) override { return *this; } size_t shards() const override { return 1; } static uint16_t encodeWay( diff --git a/include/way_store.h b/include/way_store.h index 5e274a5c..c2b959c7 100644 --- a/include/way_store.h +++ b/include/way_store.h @@ -17,13 +17,13 @@ class WayStore { virtual std::vector at(WayID wayid) const = 0; virtual bool requiresNodes() const = 0; virtual void insertLatpLons(std::vector& newWays) = 0; - virtual const void insertNodes(const std::vector>>& newWays) = 0; + virtual void insertNodes(const std::vector>>& newWays) = 0; virtual void clear() = 0; virtual std::size_t size() const = 0; virtual void finalize(unsigned int threadNum) = 0; virtual bool contains(size_t shard, WayID id) const = 0; - virtual size_t shard() const = 0; + virtual WayStore& shard(size_t shard) = 0; virtual size_t shards() const = 0; }; diff --git a/include/way_stores.h b/include/way_stores.h index 4ed8db7e..f66e3939 100644 --- a/include/way_stores.h +++ b/include/way_stores.h @@ -5,6 +5,7 @@ #include #include "way_store.h" #include "sorted_way_store.h" +#include "sharded_way_store.h" class BinarySearchWayStore: public WayStore { @@ -16,13 +17,13 @@ class BinarySearchWayStore: public WayStore { std::vector at(WayID wayid) const override; bool requiresNodes() const override { return false; } void insertLatpLons(std::vector &newWays) override; - const void insertNodes(const std::vector>>& newWays) override; + void insertNodes(const std::vector>>& newWays) override; void clear() override; std::size_t size() const override; void finalize(unsigned int threadNum) override; bool contains(size_t shard, WayID id) const override; - size_t shard() const override { return 0; } + WayStore& shard(size_t shard) override { return *this; } size_t shards() const override { return 1; } private: diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 0202a67d..f371cded 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -73,7 +73,14 @@ bool PbfReader::ReadNodes(OsmLuaProcessing &output, PrimitiveGroup &pg, Primitiv return false; } -bool PbfReader::ReadWays(OsmLuaProcessing &output, PrimitiveGroup &pg, PrimitiveBlock const &pb, bool locationsOnWays) { +bool PbfReader::ReadWays( + OsmLuaProcessing &output, + PrimitiveGroup &pg, + PrimitiveBlock const &pb, + bool locationsOnWays, + uint shard, + uint effectiveShards +) { // ---- Read ways if (pg.ways_size() > 0) { @@ -83,15 +90,18 @@ bool PbfReader::ReadWays(OsmLuaProcessing &output, PrimitiveGroup &pg, Primitive std::vector llWays; std::vector>> nodeWays; + LatpLonVec llVec; + std::vector nodeVec; for (int j=0; j(pbfWay.id()); if (wayId >= pow(2,42)) throw std::runtime_error("Way ID negative or too large: "+std::to_string(wayId)); // Assemble nodelist - LatpLonVec llVec; - std::vector nodeVec; if (locationsOnWays) { int lat=0, lon=0; llVec.reserve(pbfWay.lats_size()); @@ -105,8 +115,17 @@ bool PbfReader::ReadWays(OsmLuaProcessing &output, PrimitiveGroup &pg, Primitive int64_t nodeId = 0; llVec.reserve(pbfWay.refs_size()); nodeVec.reserve(pbfWay.refs_size()); + + bool skipToNext = false; + for (int k=0; k 1 && !osmStore.nodes.contains(shard, nodeId)) { + skipToNext = true; + break; + } + try { llVec.push_back(osmStore.nodes.at(static_cast(nodeId))); nodeVec.push_back(nodeId); @@ -114,6 +133,9 @@ bool PbfReader::ReadWays(OsmLuaProcessing &output, PrimitiveGroup &pg, Primitive if (osmStore.integrity_enforced()) throw err; } } + + if (skipToNext) + continue; } if (llVec.empty()) continue; @@ -138,9 +160,9 @@ bool PbfReader::ReadWays(OsmLuaProcessing &output, PrimitiveGroup &pg, Primitive } if (wayStoreRequiresNodes) { - osmStore.ways.insertNodes(nodeWays); + osmStore.ways.shard(shard).insertNodes(nodeWays); } else { - osmStore.ways.insertLatpLons(llWays); + osmStore.ways.shard(shard).insertLatpLons(llWays); } return true; @@ -244,7 +266,9 @@ bool PbfReader::ReadBlock( const BlockMetadata& blockMetadata, const unordered_set& nodeKeys, bool locationsOnWays, - ReadPhase phase + ReadPhase phase, + uint shard, + uint effectiveShards ) { infile.seekg(blockMetadata.offset); @@ -274,6 +298,9 @@ bool PbfReader::ReadBlock( std::ostringstream str; str << "\r"; void_mmap_allocator::reportStoreSize(str); + if (effectiveShards > 1) + str << std::to_string(shard + 1) << "/" << std::to_string(effectiveShards) << " "; + str << "Block " << blocksProcessed.load() << "/" << blocksToProcess.load() << " ways " << pg.ways_size() << " relations " << pg.relations_size() << " "; std::cout << str.str(); std::cout.flush(); @@ -304,7 +331,7 @@ bool PbfReader::ReadBlock( } if(phase == ReadPhase::Ways) { - bool done = ReadWays(output, pg, pb, locationsOnWays); + bool done = ReadWays(output, pg, pb, locationsOnWays, shard, effectiveShards); if(done) { output_progress(); ++read_groups; @@ -336,7 +363,7 @@ bool PbfReader::ReadBlock( // We can only delete blocks if we're confident we've processed everything, // which is not possible in the case of subdivided blocks. - return blockMetadata.chunks == 1; + return (shard + 1 == effectiveShards) && blockMetadata.chunks == 1; } bool blockHasPrimitiveGroupSatisfying( @@ -366,6 +393,7 @@ bool blockHasPrimitiveGroupSatisfying( } int PbfReader::ReadPbfFile( + uint shards, bool hasSortTypeThenID, unordered_set const& nodeKeys, unsigned int threadNum, @@ -463,95 +491,105 @@ int PbfReader::ReadPbfFile( std::vector all_phases = { ReadPhase::Nodes, ReadPhase::RelationScan, ReadPhase::Ways, ReadPhase::Relations }; for(auto phase: all_phases) { + uint effectiveShards = 1; + + // On memory-constrained machines, we might read ways multiple times in order + // to keep the working set of nodes limited. + if (phase == ReadPhase::Ways) + effectiveShards = shards; + + for (int shard = 0; shard < effectiveShards; shard++) { #ifdef CLOCK_MONOTONIC - timespec start, end; - clock_gettime(CLOCK_MONOTONIC, &start); + timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); #endif - // Launch the pool with threadNum threads - boost::asio::thread_pool pool(threadNum); - std::mutex block_mutex; - - // If we're in ReadPhase::Relations and there aren't many blocks left - // to read, increase parallelism by letting each thread only process - // a portion of the block. - if (phase == ReadPhase::Relations && blocks.size() < threadNum * 2) { - std::cout << "only " << blocks.size() << " relation blocks; subdividing for better parallelism" << std::endl; - std::map moreBlocks; - for (const auto& block : blocks) { - BlockMetadata newBlock = block.second; - newBlock.chunks = threadNum; - for (size_t i = 0; i < threadNum; i++) { - newBlock.chunk = i; - moreBlocks[moreBlocks.size()] = newBlock; + // Launch the pool with threadNum threads + boost::asio::thread_pool pool(threadNum); + std::mutex block_mutex; + + // If we're in ReadPhase::Relations and there aren't many blocks left + // to read, increase parallelism by letting each thread only process + // a portion of the block. + if (phase == ReadPhase::Relations && blocks.size() < threadNum * 2) { + std::cout << "only " << blocks.size() << " relation blocks; subdividing for better parallelism" << std::endl; + std::map moreBlocks; + for (const auto& block : blocks) { + BlockMetadata newBlock = block.second; + newBlock.chunks = threadNum; + for (size_t i = 0; i < threadNum; i++) { + newBlock.chunk = i; + moreBlocks[moreBlocks.size()] = newBlock; + } } + blocks = moreBlocks; } - blocks = moreBlocks; - } - std::deque> blockRanges; - std::map filteredBlocks; - for (const auto& entry : blocks) { - if ((phase == ReadPhase::Nodes && entry.second.hasNodes) || - (phase == ReadPhase::RelationScan && entry.second.hasRelations) || - (phase == ReadPhase::Ways && entry.second.hasWays) || - (phase == ReadPhase::Relations && entry.second.hasRelations)) - filteredBlocks[entry.first] = entry.second; - } + std::deque> blockRanges; + std::map filteredBlocks; + for (const auto& entry : blocks) { + if ((phase == ReadPhase::Nodes && entry.second.hasNodes) || + (phase == ReadPhase::RelationScan && entry.second.hasRelations) || + (phase == ReadPhase::Ways && entry.second.hasWays) || + (phase == ReadPhase::Relations && entry.second.hasRelations)) + filteredBlocks[entry.first] = entry.second; + } - blocksToProcess = filteredBlocks.size(); - blocksProcessed = 0; - - // When processing blocks, we try to give each worker large batches - // of contiguous blocks, so that they might benefit from long runs - // of sorted indexes, and locality of nearby IDs. - const size_t batchSize = (filteredBlocks.size() / (threadNum * 8)) + 1; - - size_t consumed = 0; - auto it = filteredBlocks.begin(); - while(it != filteredBlocks.end()) { - std::vector blockRange; - blockRange.reserve(batchSize); - size_t max = consumed + batchSize; - for (; consumed < max && it != filteredBlocks.end(); consumed++) { - IndexedBlockMetadata ibm; - memcpy(&ibm, &it->second, sizeof(BlockMetadata)); - ibm.index = it->first; - blockRange.push_back(ibm); - it++; + blocksToProcess = filteredBlocks.size(); + blocksProcessed = 0; + + // When processing blocks, we try to give each worker large batches + // of contiguous blocks, so that they might benefit from long runs + // of sorted indexes, and locality of nearby IDs. + const size_t batchSize = (filteredBlocks.size() / (threadNum * 8)) + 1; + + size_t consumed = 0; + auto it = filteredBlocks.begin(); + while(it != filteredBlocks.end()) { + std::vector blockRange; + blockRange.reserve(batchSize); + size_t max = consumed + batchSize; + for (; consumed < max && it != filteredBlocks.end(); consumed++) { + IndexedBlockMetadata ibm; + memcpy(&ibm, &it->second, sizeof(BlockMetadata)); + ibm.index = it->first; + blockRange.push_back(ibm); + it++; + } + blockRanges.push_back(blockRange); } - blockRanges.push_back(blockRange); - } - { - for(const std::vector& blockRange: blockRanges) { - boost::asio::post(pool, [=, &blockRange, &blocks, &block_mutex, &nodeKeys]() { - if (phase == ReadPhase::Nodes) - osmStore.nodes.batchStart(); - if (phase == ReadPhase::Ways) - osmStore.ways.batchStart(); - - for (const IndexedBlockMetadata& indexedBlockMetadata: blockRange) { - auto infile = generate_stream(); - auto output = generate_output(); - - if(ReadBlock(*infile, *output, indexedBlockMetadata, nodeKeys, locationsOnWays, phase)) { - const std::lock_guard lock(block_mutex); - blocks.erase(indexedBlockMetadata.index); + { + for(const std::vector& blockRange: blockRanges) { + boost::asio::post(pool, [=, &blockRange, &blocks, &block_mutex, &nodeKeys]() { + if (phase == ReadPhase::Nodes) + osmStore.nodes.batchStart(); + if (phase == ReadPhase::Ways) + osmStore.ways.batchStart(); + + for (const IndexedBlockMetadata& indexedBlockMetadata: blockRange) { + auto infile = generate_stream(); + auto output = generate_output(); + + if(ReadBlock(*infile, *output, indexedBlockMetadata, nodeKeys, locationsOnWays, phase, shard, effectiveShards)) { + const std::lock_guard lock(block_mutex); + blocks.erase(indexedBlockMetadata.index); + } + blocksProcessed++; } - blocksProcessed++; - } - }); + }); + } } - } - - pool.join(); + + pool.join(); #ifdef CLOCK_MONOTONIC - clock_gettime(CLOCK_MONOTONIC, &end); - uint64_t elapsedNs = 1e9 * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; - std::cout << "(" << std::to_string((uint32_t)(elapsedNs / 1e6)) << " ms)" << std::endl; + clock_gettime(CLOCK_MONOTONIC, &end); + uint64_t elapsedNs = 1e9 * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; + std::cout << "(" << std::to_string((uint32_t)(elapsedNs / 1e6)) << " ms)" << std::endl; #endif + } + if(phase == ReadPhase::Nodes) { osmStore.nodes.finalize(threadNum); } diff --git a/src/sharded_node_store.cpp b/src/sharded_node_store.cpp index 92169986..3bb38563 100644 --- a/src/sharded_node_store.cpp +++ b/src/sharded_node_store.cpp @@ -1,5 +1,7 @@ #include "sharded_node_store.h" +thread_local size_t lastNodeShard = 0; + ShardedNodeStore::ShardedNodeStore(std::function()> createNodeStore): createNodeStore(createNodeStore) { for (int i = 0; i < shards(); i++) @@ -20,11 +22,17 @@ void ShardedNodeStore::finalize(size_t threadNum) { } LatpLon ShardedNodeStore::at(NodeID id) const { - // TODO: look in the last store we successfully found something, using - // a thread local - for (int i = 0; i < shards(); i++) - if (stores[i]->contains(0, id) || i == shards() - 1) - return stores[i]->at(id); + for (int i = 0; i < shards(); i++) { + size_t index = (lastNodeShard + i) % shards(); + + if (stores[index]->contains(0, id)) { + lastNodeShard = index; + return stores[index]->at(id); + } + } + + // Superfluous return to silence a compiler warning + return stores[shards() - 1]->at(id); } size_t ShardedNodeStore::size() const { diff --git a/src/sharded_way_store.cpp b/src/sharded_way_store.cpp new file mode 100644 index 00000000..f4285ff5 --- /dev/null +++ b/src/sharded_way_store.cpp @@ -0,0 +1,77 @@ +#include "sharded_way_store.h" +#include "node_store.h" + +thread_local size_t lastWayShard = 0; + +ShardedWayStore::ShardedWayStore(std::function()> createWayStore, const NodeStore& nodeStore): + createWayStore(createWayStore), + nodeStore(nodeStore) { + for (int i = 0; i < shards(); i++) + stores.push_back(createWayStore()); +} + +ShardedWayStore::~ShardedWayStore() { +} + +void ShardedWayStore::reopen() { + for (auto& store : stores) + store->reopen(); +} + +void ShardedWayStore::batchStart() { + for (auto& store : stores) + store->batchStart(); +} + +std::vector ShardedWayStore::at(WayID wayid) const { + for (int i = 0; i < shards(); i++) { + size_t index = (lastWayShard + i) % shards(); + if (stores[index]->contains(0, wayid)) { + lastWayShard = index; + return stores[index]->at(wayid); + } + } + + // Superfluous return to silence a compiler warning + return stores[shards() - 1]->at(wayid); +} + +bool ShardedWayStore::requiresNodes() const { + return stores[0]->requiresNodes(); +} + +void ShardedWayStore::insertLatpLons(std::vector &newWays) { + throw std::runtime_error("ShardedWayStore::insertLatpLons: don't call this directly"); +} + +void ShardedWayStore::insertNodes(const std::vector>>& newWays) { + throw std::runtime_error("ShardedWayStore::insertNodes: don't call this directly"); +} + +void ShardedWayStore::clear() { + for (auto& store : stores) + store->clear(); +} + +std::size_t ShardedWayStore::size() const { + size_t rv = 0; + for (auto& store : stores) + rv += store->size(); + return rv; +} + +void ShardedWayStore::finalize(unsigned int threadNum) { + for (auto& store : stores) + store->finalize(threadNum); +} + +bool ShardedWayStore::contains(size_t shard, WayID id) const { + return stores[shard]->contains(0, id); +} + +WayStore& ShardedWayStore::shard(size_t shard) { + return *stores[shard].get(); +} + +size_t ShardedWayStore::shards() const { return nodeStore.shards(); } + diff --git a/src/sorted_way_store.cpp b/src/sorted_way_store.cpp index e7ff4841..27ae6ae2 100644 --- a/src/sorted_way_store.cpp +++ b/src/sorted_way_store.cpp @@ -208,7 +208,7 @@ void SortedWayStore::insertLatpLons(std::vector &newWays throw std::runtime_error("SortedWayStore does not support insertLatpLons"); } -const void SortedWayStore::insertNodes(const std::vector>>& newWays) { +void SortedWayStore::insertNodes(const std::vector>>& newWays) { // read_pbf can call with an empty array if the only ways it read were unable to // be processed due to missing nodes, so be robust against empty way vector. if (newWays.empty()) diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index 3a32168a..e6f791de 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -171,7 +171,7 @@ int main(int argc, char* argv[]) { uint threadNum; string outputFile; string bbox; - bool _verbose = false, sqlite= false, mergeSqlite = false, mapsplit = false, osmStoreCompact = false, skipIntegrity = false, osmStoreUncompressedNodes = false, osmStoreUncompressedWays = false, materializeGeometries = false; + bool _verbose = false, sqlite= false, mergeSqlite = false, mapsplit = false, osmStoreCompact = false, skipIntegrity = false, osmStoreUncompressedNodes = false, osmStoreUncompressedWays = false, materializeGeometries = false, shardStores = false; bool logTileTimings = false; po::options_description desc("tilemaker " STR(TM_VERSION) "\nConvert OpenStreetMap .pbf files into vector tiles\n\nAvailable options"); @@ -188,6 +188,7 @@ int main(int argc, char* argv[]) { ("no-compress-nodes", po::bool_switch(&osmStoreUncompressedNodes), "Store nodes uncompressed") ("no-compress-ways", po::bool_switch(&osmStoreUncompressedWays), "Store ways uncompressed") ("materialize-geometries", po::bool_switch(&materializeGeometries), "Materialize geometries - faster, but requires more memory") + ("shard-stores", po::bool_switch(&shardStores), "Shard stores - use an alternate reading/writing strategy for low-memory machines") ("verbose",po::bool_switch(&_verbose), "verbose error output") ("skip-integrity",po::bool_switch(&skipIntegrity), "don't enforce way/node integrity") ("log-tile-timings", po::bool_switch(&logTileTimings), "log how long each tile takes") @@ -311,8 +312,7 @@ int main(int argc, char* argv[]) { shared_ptr nodeStore; - // TODO: make this a flag - if (true) { + if (shardStores) { nodeStore = std::make_shared(createNodeStore); } else { nodeStore = createNodeStore(); @@ -328,7 +328,12 @@ int main(int argc, char* argv[]) { return rv; }; - shared_ptr wayStore = createWayStore(); + shared_ptr wayStore; + if (shardStores) { + wayStore = std::make_shared(createWayStore, *nodeStore.get()); + } else { + wayStore = createWayStore(); + } OSMStore osmStore(*nodeStore.get(), *wayStore.get()); osmStore.use_compact_store(osmStoreCompact); @@ -389,6 +394,7 @@ int main(int argc, char* argv[]) { const bool hasSortTypeThenID = PbfHasOptionalFeature(inputFile, OptionSortTypeThenID); int ret = pbfReader.ReadPbfFile( + nodeStore->shards(), hasSortTypeThenID, nodeKeys, threadNum, @@ -477,6 +483,7 @@ int main(int argc, char* argv[]) { vector pbf = mapsplitFile.readTile(srcZ,srcX,tmsY); int ret = pbfReader.ReadPbfFile( + nodeStore->shards(), false, nodeKeys, 1, diff --git a/src/way_stores.cpp b/src/way_stores.cpp index e19cbf5a..790ad816 100644 --- a/src/way_stores.cpp +++ b/src/way_stores.cpp @@ -47,7 +47,7 @@ void BinarySearchWayStore::insertLatpLons(std::vector &n std::copy(std::make_move_iterator(newWays.begin()), std::make_move_iterator(newWays.end()), mLatpLonLists->begin() + i); } -const void BinarySearchWayStore::insertNodes(const std::vector>>& newWays) { +void BinarySearchWayStore::insertNodes(const std::vector>>& newWays) { throw std::runtime_error("BinarySearchWayStore does not support insertNodes"); } From 4bfca704a7a6d8ae034e9f31c982baa1c6ef8bac Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 18 Dec 2023 09:11:04 -0500 Subject: [PATCH 38/81] fewer, more balanced shards --- src/sharded_node_store.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/sharded_node_store.cpp b/src/sharded_node_store.cpp index 3bb38563..4c222187 100644 --- a/src/sharded_node_store.cpp +++ b/src/sharded_node_store.cpp @@ -53,13 +53,19 @@ size_t pickStore(const LatpLon& el) { // Europe still basically gets its own bucket, but probably should be split up // more. - const size_t z3x = lon2tilex(el.lon / 10000000, 3); - const size_t z3y = latp2tiley(el.latp / 10000000, 3); + const size_t z4x = lon2tilex(el.lon / 10000000, 4); + const size_t z4y = latp2tiley(el.latp / 10000000, 4); + + const size_t z3x = z4x / 2; + const size_t z3y = z4y / 2; - if (z3x == 4 && z3y == 2) return 4; // Central Europe if (z3x == 5 && z3y == 2) return 5; // Western Russia - if (z3x == 4 && z3y == 3) return 6; // North Africa - if (z3x == 5 && z3y == 3) return 7; // India + if (z3x == 4 && z3y == 3) return 5; // North Africa + if (z3x == 5 && z3y == 3) return 5; // India + + if (z4x == 8 && z4y == 5) return 4; // some of Central Europe + + if (z3x == 4 && z3y == 2) return 3; // rest of Central Europe const size_t z2x = z3x / 2; const size_t z2y = z3y / 2; @@ -69,7 +75,7 @@ size_t pickStore(const LatpLon& el) { if (z2x == 0 && z2y == 1) return 1; // North America // std::cout << "z2x=" << std::to_string(z2x) << ", z2y=" << std::to_string(z2y) << std::endl; - return 0; // Artic, Antartcica, Oceania + return 0; // Artic, Antartcica, Oceania, South Africa, South America } void ShardedNodeStore::insert(const std::vector& elements) { @@ -90,5 +96,5 @@ bool ShardedNodeStore::contains(size_t shard, NodeID id) const { } size_t ShardedNodeStore::shards() const { - return 8; + return 6; } From ffbd1942bd44bf743dfa1cc018e9e26be869d6d4 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 18 Dec 2023 09:17:32 -0500 Subject: [PATCH 39/81] skip ReadPhase::Ways passes if node store is empty --- include/node_store.h | 2 ++ include/node_stores.h | 4 ++++ include/read_pbf.h | 4 +++- include/sharded_node_store.h | 2 ++ include/sorted_node_store.h | 2 ++ src/read_pbf.cpp | 9 ++++++++- src/tilemaker.cpp | 8 ++++++-- 7 files changed, 27 insertions(+), 4 deletions(-) diff --git a/include/node_store.h b/include/node_store.h index a2547fd5..76fe18b3 100644 --- a/include/node_store.h +++ b/include/node_store.h @@ -25,6 +25,8 @@ class NodeStore virtual LatpLon at(NodeID i) const = 0; virtual bool contains(size_t shard, NodeID id) const = 0; + virtual NodeStore& shard(size_t shard) = 0; + virtual const NodeStore& shard(size_t shard) const = 0; virtual size_t shards() const = 0; }; diff --git a/include/node_stores.h b/include/node_stores.h index 80a94868..2ef14b70 100644 --- a/include/node_stores.h +++ b/include/node_stores.h @@ -26,6 +26,8 @@ class BinarySearchNodeStore : public NodeStore void batchStart() {} bool contains(size_t shard, NodeID id) const override; + NodeStore& shard(size_t shard) override { return *this; } + const NodeStore& shard(size_t shard) const override { return *this; } size_t shards() const override { return 1; } @@ -59,6 +61,8 @@ class CompactNodeStore : public NodeStore // CompactNodeStore has no metadata to know whether or not it contains // a node, so it's not suitable for used in sharded scenarios. bool contains(size_t shard, NodeID id) const override { return true; } + NodeStore& shard(size_t shard) override { return *this; } + const NodeStore& shard(size_t shard) const override { return *this; } size_t shards() const override { return 1; } private: diff --git a/include/read_pbf.h b/include/read_pbf.h index 4ab44612..a9ffe29c 100644 --- a/include/read_pbf.h +++ b/include/read_pbf.h @@ -58,7 +58,9 @@ class PbfReader const std::unordered_set& nodeKeys, unsigned int threadNum, const pbfreader_generate_stream& generate_stream, - const pbfreader_generate_output& generate_output + const pbfreader_generate_output& generate_output, + const NodeStore& nodeStore, + const WayStore& wayStore ); // Read tags into a map from a way/node/relation diff --git a/include/sharded_node_store.h b/include/sharded_node_store.h index 44938126..ef001347 100644 --- a/include/sharded_node_store.h +++ b/include/sharded_node_store.h @@ -20,6 +20,8 @@ class ShardedNodeStore : public NodeStore { } bool contains(size_t shard, NodeID id) const override; + NodeStore& shard(size_t shard) override { return *stores[shard]; } + const NodeStore& shard(size_t shard) const override { return *stores[shard]; } size_t shards() const override; private: diff --git a/include/sorted_node_store.h b/include/sorted_node_store.h index 0e8d2e24..e2832df8 100644 --- a/include/sorted_node_store.h +++ b/include/sorted_node_store.h @@ -71,6 +71,8 @@ class SortedNodeStore : public NodeStore } bool contains(size_t shard, NodeID id) const override; + NodeStore& shard(size_t shard) override { return *this; } + const NodeStore& shard(size_t shard) const override { return *this; } size_t shards() const override { return 1; } private: diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index f371cded..d2395bde 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -398,7 +398,9 @@ int PbfReader::ReadPbfFile( unordered_set const& nodeKeys, unsigned int threadNum, const pbfreader_generate_stream& generate_stream, - const pbfreader_generate_output& generate_output + const pbfreader_generate_output& generate_output, + const NodeStore& nodeStore, + const WayStore& wayStore ) { auto infile = generate_stream(); @@ -499,6 +501,11 @@ int PbfReader::ReadPbfFile( effectiveShards = shards; for (int shard = 0; shard < effectiveShards; shard++) { + // If we're in ReadPhase::Ways, only do a pass if there is at least one + // entry in the pass's shard. + if (phase == ReadPhase::Ways && nodeStore.shard(shard).size() == 0) + continue; + #ifdef CLOCK_MONOTONIC timespec start, end; clock_gettime(CLOCK_MONOTONIC, &start); diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index e6f791de..1cb95a95 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -405,7 +405,9 @@ int main(int argc, char* argv[]) { [&]() { thread_local std::shared_ptr osmLuaProcessing(new OsmLuaProcessing(osmStore, config, layers, luaFile, shpMemTiles, osmMemTiles, attributeStore, materializeGeometries)); return osmLuaProcessing; - } + }, + *nodeStore, + *wayStore ); if (ret != 0) return ret; } @@ -492,7 +494,9 @@ int main(int argc, char* argv[]) { }, [&]() { return std::make_unique(osmStore, config, layers, luaFile, shpMemTiles, osmMemTiles, attributeStore, materializeGeometries); - } + }, + *nodeStore, + *wayStore ); if (ret != 0) return ret; From 0affec49fdd64f29137f1039d205076170eaed2b Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 18 Dec 2023 09:28:11 -0500 Subject: [PATCH 40/81] support multiple passes for ReadPhase::Relations --- include/read_pbf.h | 4 +++- include/sharded_way_store.h | 1 + include/sorted_way_store.h | 1 + include/way_store.h | 1 + include/way_stores.h | 1 + src/read_pbf.cpp | 25 ++++++++++++++++++++----- src/sharded_way_store.cpp | 4 ++++ 7 files changed, 31 insertions(+), 6 deletions(-) diff --git a/include/read_pbf.h b/include/read_pbf.h index a9ffe29c..94adb8e0 100644 --- a/include/read_pbf.h +++ b/include/read_pbf.h @@ -101,7 +101,9 @@ class PbfReader OsmLuaProcessing& output, PrimitiveGroup& pg, const PrimitiveBlock& pb, - const BlockMetadata& blockMetadata + const BlockMetadata& blockMetadata, + uint shard, + uint effectiveShards ); inline bool RelationIsType(Relation const &rel, int typeKey, int val) { diff --git a/include/sharded_way_store.h b/include/sharded_way_store.h index b57d03e0..40a3d331 100644 --- a/include/sharded_way_store.h +++ b/include/sharded_way_store.h @@ -23,6 +23,7 @@ class ShardedWayStore : public WayStore { bool contains(size_t shard, WayID id) const override; WayStore& shard(size_t shard) override; + const WayStore& shard(size_t shard) const override; size_t shards() const override; private: diff --git a/include/sorted_way_store.h b/include/sorted_way_store.h index 890a9a53..b99ba7de 100644 --- a/include/sorted_way_store.h +++ b/include/sorted_way_store.h @@ -97,6 +97,7 @@ class SortedWayStore: public WayStore { bool contains(size_t shard, WayID id) const override; WayStore& shard(size_t shard) override { return *this; } + const WayStore& shard(size_t shard) const override { return *this; } size_t shards() const override { return 1; } static uint16_t encodeWay( diff --git a/include/way_store.h b/include/way_store.h index c2b959c7..36862344 100644 --- a/include/way_store.h +++ b/include/way_store.h @@ -24,6 +24,7 @@ class WayStore { virtual bool contains(size_t shard, WayID id) const = 0; virtual WayStore& shard(size_t shard) = 0; + virtual const WayStore& shard(size_t shard) const = 0; virtual size_t shards() const = 0; }; diff --git a/include/way_stores.h b/include/way_stores.h index f66e3939..0f94e845 100644 --- a/include/way_stores.h +++ b/include/way_stores.h @@ -24,6 +24,7 @@ class BinarySearchWayStore: public WayStore { bool contains(size_t shard, WayID id) const override; WayStore& shard(size_t shard) override { return *this; } + const WayStore& shard(size_t shard) const override { return *this; } size_t shards() const override { return 1; } private: diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index d2395bde..55d2a6f3 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -206,7 +206,9 @@ bool PbfReader::ReadRelations( OsmLuaProcessing& output, PrimitiveGroup& pg, const PrimitiveBlock& pb, - const BlockMetadata& blockMetadata + const BlockMetadata& blockMetadata, + uint shard, + uint effectiveShards ) { // ---- Read relations @@ -232,15 +234,24 @@ bool PbfReader::ReadRelations( WayVec outerWayVec, innerWayVec; int64_t lastID = 0; bool isInnerOuter = isBoundary || isMultiPolygon; + bool skipToNext = false; for (int n=0; n < pbfRelation.memids_size(); n++) { lastID += pbfRelation.memids(n); if (pbfRelation.types(n) != Relation_MemberType_WAY) { continue; } int32_t role = pbfRelation.roles_sid(n); if (role==innerKey || role==outerKey) isInnerOuter=true; WayID wayId = static_cast(lastID); + + if (n == 0 && effectiveShards > 0 && !osmStore.ways.contains(shard, wayId)) { + skipToNext = true; + break; + } (role == innerKey ? innerWayVec : outerWayVec).push_back(wayId); } + if (skipToNext) + continue; + try { tag_map_t tags; readTags(pbfRelation, pb, tags); @@ -340,7 +351,7 @@ bool PbfReader::ReadBlock( } if(phase == ReadPhase::Relations) { - bool done = ReadRelations(output, pg, pb, blockMetadata); + bool done = ReadRelations(output, pg, pb, blockMetadata, shard, effectiveShards); if(done) { output_progress(); ++read_groups; @@ -495,9 +506,9 @@ int PbfReader::ReadPbfFile( for(auto phase: all_phases) { uint effectiveShards = 1; - // On memory-constrained machines, we might read ways multiple times in order - // to keep the working set of nodes limited. - if (phase == ReadPhase::Ways) + // On memory-constrained machines, we might read ways/relations + // multiple times in order to keep the working set of nodes limited. + if (phase == ReadPhase::Ways || phase == ReadPhase::Relations) effectiveShards = shards; for (int shard = 0; shard < effectiveShards; shard++) { @@ -506,6 +517,10 @@ int PbfReader::ReadPbfFile( if (phase == ReadPhase::Ways && nodeStore.shard(shard).size() == 0) continue; + // Ditto, but for relations + if (phase == ReadPhase::Relations && wayStore.shard(shard).size() == 0) + continue; + #ifdef CLOCK_MONOTONIC timespec start, end; clock_gettime(CLOCK_MONOTONIC, &start); diff --git a/src/sharded_way_store.cpp b/src/sharded_way_store.cpp index f4285ff5..d9741082 100644 --- a/src/sharded_way_store.cpp +++ b/src/sharded_way_store.cpp @@ -73,5 +73,9 @@ WayStore& ShardedWayStore::shard(size_t shard) { return *stores[shard].get(); } +const WayStore& ShardedWayStore::shard(size_t shard) const { + return *stores[shard].get(); +} + size_t ShardedWayStore::shards() const { return nodeStore.shards(); } From 3a2c87aab8c24308934bf7e2f9ea0c40591c63f2 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 18 Dec 2023 09:32:16 -0500 Subject: [PATCH 41/81] fix check for first way --- src/read_pbf.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 55d2a6f3..2d1e73fc 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -235,6 +235,7 @@ bool PbfReader::ReadRelations( int64_t lastID = 0; bool isInnerOuter = isBoundary || isMultiPolygon; bool skipToNext = false; + bool firstWay = true; for (int n=0; n < pbfRelation.memids_size(); n++) { lastID += pbfRelation.memids(n); if (pbfRelation.types(n) != Relation_MemberType_WAY) { continue; } @@ -242,10 +243,12 @@ bool PbfReader::ReadRelations( if (role==innerKey || role==outerKey) isInnerOuter=true; WayID wayId = static_cast(lastID); - if (n == 0 && effectiveShards > 0 && !osmStore.ways.contains(shard, wayId)) { + if (firstWay && effectiveShards > 0 && !osmStore.ways.contains(shard, wayId)) { skipToNext = true; break; } + if (firstWay) + firstWay = false; (role == innerKey ? innerWayVec : outerWayVec).push_back(wayId); } From f499e344a8b90ec9aa24f8cd75c6338a92e86b45 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 18 Dec 2023 17:47:49 -0500 Subject: [PATCH 42/81] adjust shards With this distribution, no node shard is more than ~8.5GB. --- src/sharded_node_store.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/sharded_node_store.cpp b/src/sharded_node_store.cpp index 4c222187..964d61fa 100644 --- a/src/sharded_node_store.cpp +++ b/src/sharded_node_store.cpp @@ -49,21 +49,24 @@ void ShardedNodeStore::batchStart() { } size_t pickStore(const LatpLon& el) { - // Assign the element to a store. This is pretty naive, we could likely do better-- - // Europe still basically gets its own bucket, but probably should be split up - // more. + // Assign the element to a shard. This is a pretty naive division + // of the globe, tuned to have max ~10GB of nodes/ways per shard. - const size_t z4x = lon2tilex(el.lon / 10000000, 4); - const size_t z4y = latp2tiley(el.latp / 10000000, 4); + const size_t z5x = lon2tilex(el.lon / 10000000, 5); + const size_t z5y = latp2tiley(el.latp / 10000000, 5); + + const size_t z4x = z5x / 2; + const size_t z4y = z5y / 2; const size_t z3x = z4x / 2; const size_t z3y = z4y / 2; - if (z3x == 5 && z3y == 2) return 5; // Western Russia - if (z3x == 4 && z3y == 3) return 5; // North Africa - if (z3x == 5 && z3y == 3) return 5; // India + if (z3x == 5 && z3y == 2) return 6; // Western Russia + if (z3x == 4 && z3y == 3) return 6; // North Africa + if (z3x == 5 && z3y == 3) return 6; // India - if (z4x == 8 && z4y == 5) return 4; // some of Central Europe + if ((z5x == 16 && z5y == 10) || (z5x == 16 && z5y == 11)) return 5; // some of Central Europe + if ((z5x == 17 && z5y == 10) || (z5x == 17 && z5y == 11)) return 4; // some more of Central Europe if (z3x == 4 && z3y == 2) return 3; // rest of Central Europe @@ -96,5 +99,5 @@ bool ShardedNodeStore::contains(size_t shard, NodeID id) const { } size_t ShardedNodeStore::shards() const { - return 6; + return 7; } From bbf0957c1eb1cca7e35e1aa36e8a672e22a65034 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Tue, 19 Dec 2023 00:28:40 -0500 Subject: [PATCH 43/81] Relations: fix effectiveShards > 1 check Oops, bug that very moderately affected performance in the non `--shard-stores` case --- src/read_pbf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 2d1e73fc..9b8b2f15 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -243,7 +243,7 @@ bool PbfReader::ReadRelations( if (role==innerKey || role==outerKey) isInnerOuter=true; WayID wayId = static_cast(lastID); - if (firstWay && effectiveShards > 0 && !osmStore.ways.contains(shard, wayId)) { + if (firstWay && effectiveShards > 1 && !osmStore.ways.contains(shard, wayId)) { skipToNext = true; break; } From c56d337e1f28029bb7f1046cae8add59ee21f11c Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Thu, 21 Dec 2023 18:26:55 -0500 Subject: [PATCH 44/81] wip: use protozero for osm pbf reading This is the minimal set of changes to use protozero, and is quite ugly in places. It also still devolves to using std::string for tags, so we're still paying a cost for allocating long strings on the heap. I hope to look at that next. Time for the PBF read phase: GB planet-on-32gb: 1m5s, 1GB RAM this branch: 55.7s, 0.74G RAM NA planet-on-32gb: 10m49s, 3.01gb this branch: 10m10s, 2.68gb So 6-15% speedup in reading, and perhaps a fixed 250MB reduction in time. The NA case is heavily affected by the Hudson Bay relation being a straggler. Somehow tweaking things so that geometry could be read first would ensure that the gains from protozero aren't lost to the whims of the order of blocks in the PBF. --- CMakeLists.txt | 1 + Makefile | 7 + include/helpers.h | 5 +- include/pbf_reader.h | 243 ++++++ include/protozero/basic_pbf_builder.hpp | 266 ++++++ include/protozero/basic_pbf_writer.hpp | 1054 +++++++++++++++++++++++ include/protozero/buffer_fixed.hpp | 222 +++++ include/protozero/buffer_string.hpp | 78 ++ include/protozero/buffer_tmpl.hpp | 113 +++ include/protozero/buffer_vector.hpp | 78 ++ include/protozero/byteswap.hpp | 108 +++ include/protozero/config.hpp | 48 ++ include/protozero/data_view.hpp | 236 +++++ include/protozero/exception.hpp | 101 +++ include/protozero/iterators.hpp | 481 +++++++++++ include/protozero/pbf_builder.hpp | 32 + include/protozero/pbf_message.hpp | 184 ++++ include/protozero/pbf_reader.hpp | 977 +++++++++++++++++++++ include/protozero/pbf_writer.hpp | 76 ++ include/protozero/types.hpp | 66 ++ include/protozero/varint.hpp | 245 ++++++ include/protozero/version.hpp | 34 + include/read_pbf.h | 49 +- src/helpers.cpp | 29 +- src/pbf_blocks.cpp | 3 +- src/pbf_reader.cpp | 594 +++++++++++++ src/read_pbf.cpp | 461 +++++----- src/tilemaker.cpp | 8 +- test/monaco.pbf | Bin 0 -> 533899 bytes test/pbf_reader.test.cpp | 134 +++ 30 files changed, 5659 insertions(+), 274 deletions(-) create mode 100644 include/pbf_reader.h create mode 100644 include/protozero/basic_pbf_builder.hpp create mode 100644 include/protozero/basic_pbf_writer.hpp create mode 100644 include/protozero/buffer_fixed.hpp create mode 100644 include/protozero/buffer_string.hpp create mode 100644 include/protozero/buffer_tmpl.hpp create mode 100644 include/protozero/buffer_vector.hpp create mode 100644 include/protozero/byteswap.hpp create mode 100644 include/protozero/config.hpp create mode 100644 include/protozero/data_view.hpp create mode 100644 include/protozero/exception.hpp create mode 100644 include/protozero/iterators.hpp create mode 100644 include/protozero/pbf_builder.hpp create mode 100644 include/protozero/pbf_message.hpp create mode 100644 include/protozero/pbf_reader.hpp create mode 100644 include/protozero/pbf_writer.hpp create mode 100644 include/protozero/types.hpp create mode 100644 include/protozero/varint.hpp create mode 100644 include/protozero/version.hpp create mode 100644 src/pbf_reader.cpp create mode 100644 test/monaco.pbf create mode 100644 test/pbf_reader.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index dd3179bb..5e62f26d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,7 @@ file(GLOB tilemaker_src_files src/osm_store.cpp src/output_object.cpp src/pbf_blocks.cpp + src/pbf_reader.cpp src/pooled_string.cpp src/read_pbf.cpp src/read_shp.cpp diff --git a/Makefile b/Makefile index 81779d79..6861a9c0 100644 --- a/Makefile +++ b/Makefile @@ -112,6 +112,7 @@ tilemaker: \ src/output_object.o \ src/pbf_blocks.o \ src/pooled_string.o \ + src/pbf_reader.o \ src/read_pbf.o \ src/read_shp.o \ src/sharded_node_store.o \ @@ -130,6 +131,7 @@ tilemaker: \ test: \ test_append_vector \ test_attribute_store \ + test_pbf_reader \ test_pooled_string \ test_sorted_node_store \ test_sorted_way_store @@ -171,6 +173,11 @@ test_sorted_way_store: \ test/sorted_way_store.test.o $(CXX) $(CXXFLAGS) -o test.sorted_way_store $^ $(INC) $(LIB) $(LDFLAGS) && ./test.sorted_way_store +test_pbf_reader: \ + src/helpers.o \ + src/pbf_reader.o \ + test/pbf_reader.test.o + $(CXX) $(CXXFLAGS) -o test.pbf_reader $^ $(INC) $(LIB) $(LDFLAGS) && ./test.pbf_reader %.o: %.cpp $(CXX) $(CXXFLAGS) -o $@ -c $< $(INC) diff --git a/include/helpers.h b/include/helpers.h index dff072e9..55f719ad 100644 --- a/include/helpers.h +++ b/include/helpers.h @@ -3,7 +3,8 @@ #define _HELPERS_H #include -#include "geom.h" +#include +#include // General helper routines @@ -27,7 +28,7 @@ inline std::vector split_string(std::string &inputStr, char sep) { return res; } -std::string decompress_string(const std::string& str, bool asGzip = false); +void decompress_string(std::string& output, const char* input, uint32_t inputSize, bool asGzip = false); std::string compress_string(const std::string& str, int compressionlevel = Z_DEFAULT_COMPRESSION, diff --git a/include/pbf_reader.h b/include/pbf_reader.h new file mode 100644 index 00000000..b6cb9482 --- /dev/null +++ b/include/pbf_reader.h @@ -0,0 +1,243 @@ +#ifndef _PBF_READER_H +#define _PBF_READER_H + +#include +#include +#include +#include +#include +#include + +namespace PbfReader { + namespace Schema { + // See https://wiki.openstreetmap.org/wiki/PBF_Format#Definition_of_the_OSMHeader_fileblock + // for more background on the PBF schema. + enum class BlobHeader : protozero::pbf_tag_type { + required_string_type = 1, + optional_bytes_indexdata = 2, + required_int32_datasize = 3 + }; + + enum class Blob : protozero::pbf_tag_type { + optional_int32_raw_size = 2, // When compressed, the uncompressed size + oneof_data_bytes_raw = 1, // No compression + oneof_data_bytes_zlib_data = 3, + oneof_data_bytes_lzma_data = 4, + // Formerly used for bzip2 compressed data. Deprecated in 2010. + // bytes OBSOLETE_bzip2_data = 5 [deprecated=true]; // Don't reuse this tag number. + oneof_data_bytes_lz4_data = 6, + oneof_data_bytes_zstd_data = 7, + }; + + enum class HeaderBBox : protozero::pbf_tag_type { + // These units are always in nanodegrees, they don't obey granularity rules. + required_sint64_left = 1, + required_sint64_right = 2, + required_sint64_top = 3, + required_sint64_bottom = 4 + }; + + enum class HeaderBlock : protozero::pbf_tag_type { + optional_HeaderBBox_bbox = 1, + repeated_string_optional_features = 5 + }; + + enum class StringTable : protozero::pbf_tag_type { + repeated_bytes_s = 1 + }; + + enum class PrimitiveBlock : protozero::pbf_tag_type { + required_StringTable_stringtable = 1, + repeated_PrimitiveGroup_primitivegroup = 2, + optional_int32_granularity = 17, + optional_int32_date_granularity = 18, + optional_int64_lat_offset = 19, + optional_int64_lon_offset = 20 + }; + + enum class PrimitiveGroup : protozero::pbf_tag_type { + repeated_Node_nodes = 1, + optional_DenseNodes_dense = 2, + repeated_Way_ways = 3, + repeated_Relation_relations = 4, + repeated_ChangeSet_changesets = 5 + }; + + enum class DenseNodes : protozero::pbf_tag_type { + repeated_sint64_id = 1, + repeated_sint64_lat = 8, + repeated_sint64_lon = 9, + repeated_int32_keys_vals = 10 + }; + + enum class Way : protozero::pbf_tag_type { + required_int64_id = 1, + repeated_uint32_keys = 2, + repeated_uint32_vals = 3, + repeated_sint64_refs = 8, + repeated_sint64_lats = 9, + repeated_sint64_lons = 10 + }; + + enum class Relation : protozero::pbf_tag_type { + required_int64_id = 1, + repeated_uint32_keys = 2, + repeated_uint32_vals = 3, + repeated_int32_roles_sid = 8, + repeated_sint64_memids = 9, + repeated_MemberType_types = 10 + }; + } + + struct BlobHeader { + std::string type; + int32_t datasize; + }; + + struct HeaderBBox { + double minLon, maxLon, minLat, maxLat; + }; + + struct HeaderBlock { + bool hasBbox; + HeaderBBox bbox; + std::set optionalFeatures; + }; + + enum class PrimitiveGroupType: char { Node = 1, DenseNodes = 2, Way = 3, Relation = 4, ChangeSet = 5}; + + struct Nodes { + struct Node { + uint64_t id; + int32_t lon; + int32_t lat; + uint32_t tagStart; + uint32_t tagEnd; + }; + + struct Iterator { + int32_t offset; + Node node; + + bool operator!=(Iterator& other) const; + void operator++(); + Node& operator*(); + }; + Iterator begin(); + Iterator end(); + }; + + struct Way { + uint64_t id; + std::vector keys; + std::vector vals; + std::vector refs; + std::vector lats; + std::vector lons; + }; + + struct Relation { + enum MemberType: int { NODE = 0, WAY = 1, RELATION = 2 }; + uint64_t id; + std::vector keys; + std::vector vals; + std::vector memids; + std::vector roles_sid; + std::vector types; + }; + + class PrimitiveGroup; + struct Ways { + struct Iterator { + protozero::pbf_message message; + int offset; + + bool operator!=(Iterator& other) const; + void operator++(); + PbfReader::Way& operator*(); + }; + Iterator begin(); + Iterator end(); + + PrimitiveGroup* pg; + }; + + struct Relations { + struct Iterator { + protozero::pbf_message message; + int offset; + + bool operator!=(Iterator& other) const; + void operator++(); + PbfReader::Relation& operator*(); + }; + Iterator begin(); + Iterator end(); + + PrimitiveGroup* pg; + }; + + struct PrimitiveGroup { + PrimitiveGroup(protozero::data_view data); + Nodes& nodes() const; + Ways& ways() const; + Relations& relations() const; + PrimitiveGroupType type() const; + + int32_t translateNodeKeyValue(int32_t i) const; + + // Only meant to be called by our iterator, not by client code. + void ensureData(); + protozero::data_view getDataView(); + private: + mutable Ways internalWays; + mutable Relations internalRelations; + PrimitiveGroupType internalType; + protozero::data_view data; + bool denseNodesInitialized; + + }; + + struct PrimitiveBlock { + struct PrimitiveGroups { + struct Iterator { + int offset; + std::vector* groups; + + Iterator(): offset(0), groups(nullptr) {} + Iterator(int offset, std::vector& groups): offset(offset), groups(&groups) {} + bool operator!=(Iterator& other) const; + void operator++(); + PrimitiveGroup& operator*(); + }; + + std::vector* groups; + PrimitiveGroups(): groups(nullptr) {} + PrimitiveGroups(std::vector& groups): groups(&groups) {} + Iterator begin(); + Iterator end(); + }; + + std::vector stringTable; + PrimitiveGroups& groups(); + + // Not meant to be called directly by client code. + std::vector internalGroups; + // This is a hack, but my C++-fu isn't enough to know how to avoid it. + PrimitiveGroups groupsImpl; + }; + + BlobHeader readBlobHeader(std::istream& input); + protozero::data_view readBlob(int32_t datasize, std::istream& input); + HeaderBlock readHeaderBlock(protozero::data_view data); + HeaderBBox readHeaderBBox(protozero::data_view data); + PrimitiveBlock& readPrimitiveBlock(protozero::data_view data); + void readDenseNodes(protozero::data_view data); + void readWay(protozero::data_view data, Way& way); + void readRelation(protozero::data_view data, Relation& relation); + void readStringTable(protozero::data_view data, std::vector& stringTable); + + HeaderBlock readHeaderFromFile(std::istream& input); +} + +#endif diff --git a/include/protozero/basic_pbf_builder.hpp b/include/protozero/basic_pbf_builder.hpp new file mode 100644 index 00000000..0ede726f --- /dev/null +++ b/include/protozero/basic_pbf_builder.hpp @@ -0,0 +1,266 @@ +#ifndef PROTOZERO_BASIC_PBF_BUILDER_HPP +#define PROTOZERO_BASIC_PBF_BUILDER_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file basic_pbf_builder.hpp + * + * @brief Contains the basic_pbf_builder template class. + */ + +#include "basic_pbf_writer.hpp" +#include "types.hpp" + +#include + +namespace protozero { + +/** + * The basic_pbf_builder is used to write PBF formatted messages into a buffer. + * It is based on the basic_pbf_writer class and has all the same methods. The + * difference is that while the pbf_writer class takes an integer tag, + * this template class takes a tag of the template type T. The idea is that + * T will be an enumeration value and this helps reduce the possibility of + * programming errors. + * + * Almost all methods in this class can throw an std::bad_alloc exception if + * the underlying buffer class wants to resize. + * + * Read the tutorial to understand how this class is used. In most cases you + * want to use the pbf_builder class which uses a std::string as buffer type. + */ +template +class basic_pbf_builder : public basic_pbf_writer { + + static_assert(std::is_same::type>::value, + "T must be enum with underlying type protozero::pbf_tag_type"); + +public: + + /// The type of messages this class will build. + using enum_type = T; + + basic_pbf_builder() = default; + + /** + * Create a builder using the given string as a data store. The object + * stores a reference to that string and adds all data to it. The string + * doesn't have to be empty. The pbf_message object will just append data. + */ + explicit basic_pbf_builder(TBuffer& data) noexcept : + basic_pbf_writer{data} { + } + + /** + * Construct a pbf_builder for a submessage from the pbf_message or + * pbf_writer of the parent message. + * + * @param parent_writer The parent pbf_message or pbf_writer + * @param tag Tag of the field that will be written + */ + template + basic_pbf_builder(basic_pbf_writer& parent_writer, P tag) noexcept : + basic_pbf_writer{parent_writer, pbf_tag_type(tag)} { + } + +/// @cond INTERNAL +#define PROTOZERO_WRITER_WRAP_ADD_SCALAR(name, type) \ + void add_##name(T tag, type value) { \ + basic_pbf_writer::add_##name(pbf_tag_type(tag), value); \ + } + + PROTOZERO_WRITER_WRAP_ADD_SCALAR(bool, bool) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(enum, int32_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(int32, int32_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(sint32, int32_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(uint32, uint32_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(int64, int64_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(sint64, int64_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(uint64, uint64_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(fixed32, uint32_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(sfixed32, int32_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(fixed64, uint64_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(sfixed64, int64_t) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(float, float) + PROTOZERO_WRITER_WRAP_ADD_SCALAR(double, double) + +#undef PROTOZERO_WRITER_WRAP_ADD_SCALAR +/// @endcond + + /** + * Add "bytes" field to data. + * + * @param tag Tag of the field + * @param value Pointer to value to be written + * @param size Number of bytes to be written + */ + void add_bytes(T tag, const char* value, std::size_t size) { + basic_pbf_writer::add_bytes(pbf_tag_type(tag), value, size); + } + + /** + * Add "bytes" field to data. + * + * @param tag Tag of the field + * @param value Value to be written + */ + void add_bytes(T tag, const data_view& value) { + basic_pbf_writer::add_bytes(pbf_tag_type(tag), value); + } + + /** + * Add "bytes" field to data. + * + * @param tag Tag of the field + * @param value Value to be written + */ + void add_bytes(T tag, const std::string& value) { + basic_pbf_writer::add_bytes(pbf_tag_type(tag), value); + } + + /** + * Add "bytes" field to data. Bytes from the value are written until + * a null byte is encountered. The null byte is not added. + * + * @param tag Tag of the field + * @param value Pointer to zero-delimited value to be written + */ + void add_bytes(T tag, const char* value) { + basic_pbf_writer::add_bytes(pbf_tag_type(tag), value); + } + + /** + * Add "bytes" field to data using vectored input. All the data in the + * 2nd and further arguments is "concatenated" with only a single copy + * into the final buffer. + * + * This will work with objects of any type supporting the data() and + * size() methods like std::string or protozero::data_view. + * + * Example: + * @code + * std::string data1 = "abc"; + * std::string data2 = "xyz"; + * builder.add_bytes_vectored(1, data1, data2); + * @endcode + * + * @tparam Ts List of types supporting data() and size() methods. + * @param tag Tag of the field + * @param values List of objects of types Ts with data to be appended. + */ + template + void add_bytes_vectored(T tag, Ts&&... values) { + basic_pbf_writer::add_bytes_vectored(pbf_tag_type(tag), std::forward(values)...); + } + + /** + * Add "string" field to data. + * + * @param tag Tag of the field + * @param value Pointer to value to be written + * @param size Number of bytes to be written + */ + void add_string(T tag, const char* value, std::size_t size) { + basic_pbf_writer::add_string(pbf_tag_type(tag), value, size); + } + + /** + * Add "string" field to data. + * + * @param tag Tag of the field + * @param value Value to be written + */ + void add_string(T tag, const data_view& value) { + basic_pbf_writer::add_string(pbf_tag_type(tag), value); + } + + /** + * Add "string" field to data. + * + * @param tag Tag of the field + * @param value Value to be written + */ + void add_string(T tag, const std::string& value) { + basic_pbf_writer::add_string(pbf_tag_type(tag), value); + } + + /** + * Add "string" field to data. Bytes from the value are written until + * a null byte is encountered. The null byte is not added. + * + * @param tag Tag of the field + * @param value Pointer to value to be written + */ + void add_string(T tag, const char* value) { + basic_pbf_writer::add_string(pbf_tag_type(tag), value); + } + + /** + * Add "message" field to data. + * + * @param tag Tag of the field + * @param value Pointer to message to be written + * @param size Length of the message + */ + void add_message(T tag, const char* value, std::size_t size) { + basic_pbf_writer::add_message(pbf_tag_type(tag), value, size); + } + + /** + * Add "message" field to data. + * + * @param tag Tag of the field + * @param value Value to be written. The value must be a complete message. + */ + void add_message(T tag, const data_view& value) { + basic_pbf_writer::add_message(pbf_tag_type(tag), value); + } + + /** + * Add "message" field to data. + * + * @param tag Tag of the field + * @param value Value to be written. The value must be a complete message. + */ + void add_message(T tag, const std::string& value) { + basic_pbf_writer::add_message(pbf_tag_type(tag), value); + } + +/// @cond INTERNAL +#define PROTOZERO_WRITER_WRAP_ADD_PACKED(name) \ + template \ + void add_packed_##name(T tag, InputIterator first, InputIterator last) { \ + basic_pbf_writer::add_packed_##name(pbf_tag_type(tag), first, last); \ + } + + PROTOZERO_WRITER_WRAP_ADD_PACKED(bool) + PROTOZERO_WRITER_WRAP_ADD_PACKED(enum) + PROTOZERO_WRITER_WRAP_ADD_PACKED(int32) + PROTOZERO_WRITER_WRAP_ADD_PACKED(sint32) + PROTOZERO_WRITER_WRAP_ADD_PACKED(uint32) + PROTOZERO_WRITER_WRAP_ADD_PACKED(int64) + PROTOZERO_WRITER_WRAP_ADD_PACKED(sint64) + PROTOZERO_WRITER_WRAP_ADD_PACKED(uint64) + PROTOZERO_WRITER_WRAP_ADD_PACKED(fixed32) + PROTOZERO_WRITER_WRAP_ADD_PACKED(sfixed32) + PROTOZERO_WRITER_WRAP_ADD_PACKED(fixed64) + PROTOZERO_WRITER_WRAP_ADD_PACKED(sfixed64) + PROTOZERO_WRITER_WRAP_ADD_PACKED(float) + PROTOZERO_WRITER_WRAP_ADD_PACKED(double) + +#undef PROTOZERO_WRITER_WRAP_ADD_PACKED +/// @endcond + +}; // class basic_pbf_builder + +} // end namespace protozero + +#endif // PROTOZERO_BASIC_PBF_BUILDER_HPP diff --git a/include/protozero/basic_pbf_writer.hpp b/include/protozero/basic_pbf_writer.hpp new file mode 100644 index 00000000..f167c4d1 --- /dev/null +++ b/include/protozero/basic_pbf_writer.hpp @@ -0,0 +1,1054 @@ +#ifndef PROTOZERO_BASIC_PBF_WRITER_HPP +#define PROTOZERO_BASIC_PBF_WRITER_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file basic_pbf_writer.hpp + * + * @brief Contains the basic_pbf_writer template class. + */ + +#include "buffer_tmpl.hpp" +#include "config.hpp" +#include "data_view.hpp" +#include "types.hpp" +#include "varint.hpp" + +#if PROTOZERO_BYTE_ORDER != PROTOZERO_LITTLE_ENDIAN +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace protozero { + +namespace detail { + + template class packed_field_varint; + template class packed_field_svarint; + template class packed_field_fixed; + +} // end namespace detail + +/** + * The basic_pbf_writer is used to write PBF formatted messages into a buffer. + * + * This uses TBuffer as the type for the underlaying buffer. In typical uses + * this is std::string, but you can use a different type that must support + * the right interface. Please see the documentation for details. + * + * Almost all methods in this class can throw an std::bad_alloc exception if + * the underlying buffer class wants to resize. + */ +template +class basic_pbf_writer { + + // A pointer to a buffer holding the data already written to the PBF + // message. For default constructed writers or writers that have been + // rolled back, this is a nullptr. + TBuffer* m_data = nullptr; + + // A pointer to a parent writer object if this is a submessage. If this + // is a top-level writer, it is a nullptr. + basic_pbf_writer* m_parent_writer = nullptr; + + // This is usually 0. If there is an open submessage, this is set in the + // parent to the rollback position, ie. the last position before the + // submessage was started. This is the position where the header of the + // submessage starts. + std::size_t m_rollback_pos = 0; + + // This is usually 0. If there is an open submessage, this is set in the + // parent to the position where the data of the submessage is written to. + std::size_t m_pos = 0; + + void add_varint(uint64_t value) { + protozero_assert(m_pos == 0 && "you can't add fields to a parent basic_pbf_writer if there is an existing basic_pbf_writer for a submessage"); + protozero_assert(m_data); + add_varint_to_buffer(m_data, value); + } + + void add_field(pbf_tag_type tag, pbf_wire_type type) { + protozero_assert(((tag > 0 && tag < 19000) || (tag > 19999 && tag <= ((1U << 29U) - 1))) && "tag out of range"); + const uint32_t b = (tag << 3U) | uint32_t(type); + add_varint(b); + } + + void add_tagged_varint(pbf_tag_type tag, uint64_t value) { + add_field(tag, pbf_wire_type::varint); + add_varint(value); + } + + template + void add_fixed(T value) { + protozero_assert(m_pos == 0 && "you can't add fields to a parent basic_pbf_writer if there is an existing basic_pbf_writer for a submessage"); + protozero_assert(m_data); +#if PROTOZERO_BYTE_ORDER != PROTOZERO_LITTLE_ENDIAN + byteswap_inplace(&value); +#endif + buffer_customization::append(m_data, reinterpret_cast(&value), sizeof(T)); + } + + template + void add_packed_fixed(pbf_tag_type tag, It first, It last, std::input_iterator_tag /*unused*/) { + if (first == last) { + return; + } + + basic_pbf_writer sw{*this, tag}; + + while (first != last) { + sw.add_fixed(*first++); + } + } + + template + void add_packed_fixed(pbf_tag_type tag, It first, It last, std::forward_iterator_tag /*unused*/) { + if (first == last) { + return; + } + + const auto length = std::distance(first, last); + add_length_varint(tag, sizeof(T) * pbf_length_type(length)); + reserve(sizeof(T) * std::size_t(length)); + + while (first != last) { + add_fixed(*first++); + } + } + + template + void add_packed_varint(pbf_tag_type tag, It first, It last) { + if (first == last) { + return; + } + + basic_pbf_writer sw{*this, tag}; + + while (first != last) { + sw.add_varint(uint64_t(*first++)); + } + } + + template + void add_packed_svarint(pbf_tag_type tag, It first, It last) { + if (first == last) { + return; + } + + basic_pbf_writer sw{*this, tag}; + + while (first != last) { + sw.add_varint(encode_zigzag64(*first++)); + } + } + + // The number of bytes to reserve for the varint holding the length of + // a length-delimited field. The length has to fit into pbf_length_type, + // and a varint needs 8 bit for every 7 bit. + enum : int { + reserve_bytes = sizeof(pbf_length_type) * 8 / 7 + 1 + }; + + // If m_rollpack_pos is set to this special value, it means that when + // the submessage is closed, nothing needs to be done, because the length + // of the submessage has already been written correctly. + enum : std::size_t { + size_is_known = std::numeric_limits::max() + }; + + void open_submessage(pbf_tag_type tag, std::size_t size) { + protozero_assert(m_pos == 0); + protozero_assert(m_data); + if (size == 0) { + m_rollback_pos = buffer_customization::size(m_data); + add_field(tag, pbf_wire_type::length_delimited); + buffer_customization::append_zeros(m_data, std::size_t(reserve_bytes)); + } else { + m_rollback_pos = size_is_known; + add_length_varint(tag, pbf_length_type(size)); + reserve(size); + } + m_pos = buffer_customization::size(m_data); + } + + void rollback_submessage() { + protozero_assert(m_pos != 0); + protozero_assert(m_rollback_pos != size_is_known); + protozero_assert(m_data); + buffer_customization::resize(m_data, m_rollback_pos); + m_pos = 0; + } + + void commit_submessage() { + protozero_assert(m_pos != 0); + protozero_assert(m_rollback_pos != size_is_known); + protozero_assert(m_data); + const auto length = pbf_length_type(buffer_customization::size(m_data) - m_pos); + + protozero_assert(buffer_customization::size(m_data) >= m_pos - reserve_bytes); + const auto n = add_varint_to_buffer(buffer_customization::at_pos(m_data, m_pos - reserve_bytes), length); + + buffer_customization::erase_range(m_data, m_pos - reserve_bytes + n, m_pos); + m_pos = 0; + } + + void close_submessage() { + protozero_assert(m_data); + if (m_pos == 0 || m_rollback_pos == size_is_known) { + return; + } + if (buffer_customization::size(m_data) - m_pos == 0) { + rollback_submessage(); + } else { + commit_submessage(); + } + } + + void add_length_varint(pbf_tag_type tag, pbf_length_type length) { + add_field(tag, pbf_wire_type::length_delimited); + add_varint(length); + } + +public: + + /** + * Create a writer using the specified buffer as a data store. The + * basic_pbf_writer stores a pointer to that buffer and adds all data to + * it. The buffer doesn't have to be empty. The basic_pbf_writer will just + * append data. + */ + explicit basic_pbf_writer(TBuffer& buffer) noexcept : + m_data{&buffer} { + } + + /** + * Create a writer without a data store. In this form the writer can not + * be used! + */ + basic_pbf_writer() noexcept = default; + + /** + * Construct a basic_pbf_writer for a submessage from the basic_pbf_writer + * of the parent message. + * + * @param parent_writer The basic_pbf_writer + * @param tag Tag (field number) of the field that will be written + * @param size Optional size of the submessage in bytes (use 0 for unknown). + * Setting this allows some optimizations but is only possible in + * a few very specific cases. + */ + basic_pbf_writer(basic_pbf_writer& parent_writer, pbf_tag_type tag, std::size_t size = 0) : + m_data{parent_writer.m_data}, + m_parent_writer{&parent_writer} { + m_parent_writer->open_submessage(tag, size); + } + + /// A basic_pbf_writer object can not be copied + basic_pbf_writer(const basic_pbf_writer&) = delete; + + /// A basic_pbf_writer object can not be copied + basic_pbf_writer& operator=(const basic_pbf_writer&) = delete; + + /** + * A basic_pbf_writer object can be moved. After this the other + * basic_pbf_writer will be invalid. + */ + basic_pbf_writer(basic_pbf_writer&& other) noexcept : + m_data{other.m_data}, + m_parent_writer{other.m_parent_writer}, + m_rollback_pos{other.m_rollback_pos}, + m_pos{other.m_pos} { + other.m_data = nullptr; + other.m_parent_writer = nullptr; + other.m_rollback_pos = 0; + other.m_pos = 0; + } + + /** + * A basic_pbf_writer object can be moved. After this the other + * basic_pbf_writer will be invalid. + */ + basic_pbf_writer& operator=(basic_pbf_writer&& other) noexcept { + m_data = other.m_data; + m_parent_writer = other.m_parent_writer; + m_rollback_pos = other.m_rollback_pos; + m_pos = other.m_pos; + other.m_data = nullptr; + other.m_parent_writer = nullptr; + other.m_rollback_pos = 0; + other.m_pos = 0; + return *this; + } + + ~basic_pbf_writer() noexcept { + try { + if (m_parent_writer != nullptr) { + m_parent_writer->close_submessage(); + } + } catch (...) { + // This try/catch is used to make the destructor formally noexcept. + // close_submessage() is not noexcept, but will not throw the way + // it is called here, so we are good. But to be paranoid, call... + std::terminate(); + } + } + + /** + * Check if this writer is valid. A writer is invalid if it was default + * constructed, moved from, or if commit() has been called on it. + * Otherwise it is valid. + */ + bool valid() const noexcept { + return m_data != nullptr; + } + + /** + * Swap the contents of this object with the other. + * + * @param other Other object to swap data with. + */ + void swap(basic_pbf_writer& other) noexcept { + using std::swap; + swap(m_data, other.m_data); + swap(m_parent_writer, other.m_parent_writer); + swap(m_rollback_pos, other.m_rollback_pos); + swap(m_pos, other.m_pos); + } + + /** + * Reserve size bytes in the underlying message store in addition to + * whatever the message store already holds. So unlike + * the `std::string::reserve()` method this is not an absolute size, + * but additional memory that should be reserved. + * + * @param size Number of bytes to reserve in underlying message store. + */ + void reserve(std::size_t size) { + protozero_assert(m_data); + buffer_customization::reserve_additional(m_data, size); + } + + /** + * Commit this submessage. This does the same as when the basic_pbf_writer + * goes out of scope and is destructed. + * + * @pre Must be a basic_pbf_writer of a submessage, ie one opened with the + * basic_pbf_writer constructor taking a parent message. + * @post The basic_pbf_writer is invalid and can't be used any more. + */ + void commit() { + protozero_assert(m_parent_writer && "you can't call commit() on a basic_pbf_writer without a parent"); + protozero_assert(m_pos == 0 && "you can't call commit() on a basic_pbf_writer that has an open nested submessage"); + m_parent_writer->close_submessage(); + m_parent_writer = nullptr; + m_data = nullptr; + } + + /** + * Cancel writing of this submessage. The complete submessage will be + * removed as if it was never created and no fields were added. + * + * @pre Must be a basic_pbf_writer of a submessage, ie one opened with the + * basic_pbf_writer constructor taking a parent message. + * @post The basic_pbf_writer is invalid and can't be used any more. + */ + void rollback() { + protozero_assert(m_parent_writer && "you can't call rollback() on a basic_pbf_writer without a parent"); + protozero_assert(m_pos == 0 && "you can't call rollback() on a basic_pbf_writer that has an open nested submessage"); + m_parent_writer->rollback_submessage(); + m_parent_writer = nullptr; + m_data = nullptr; + } + + ///@{ + /** + * @name Scalar field writer functions + */ + + /** + * Add "bool" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_bool(pbf_tag_type tag, bool value) { + add_field(tag, pbf_wire_type::varint); + protozero_assert(m_pos == 0 && "you can't add fields to a parent basic_pbf_writer if there is an existing basic_pbf_writer for a submessage"); + protozero_assert(m_data); + m_data->push_back(char(value)); + } + + /** + * Add "enum" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_enum(pbf_tag_type tag, int32_t value) { + add_tagged_varint(tag, uint64_t(value)); + } + + /** + * Add "int32" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_int32(pbf_tag_type tag, int32_t value) { + add_tagged_varint(tag, uint64_t(value)); + } + + /** + * Add "sint32" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_sint32(pbf_tag_type tag, int32_t value) { + add_tagged_varint(tag, encode_zigzag32(value)); + } + + /** + * Add "uint32" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_uint32(pbf_tag_type tag, uint32_t value) { + add_tagged_varint(tag, value); + } + + /** + * Add "int64" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_int64(pbf_tag_type tag, int64_t value) { + add_tagged_varint(tag, uint64_t(value)); + } + + /** + * Add "sint64" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_sint64(pbf_tag_type tag, int64_t value) { + add_tagged_varint(tag, encode_zigzag64(value)); + } + + /** + * Add "uint64" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_uint64(pbf_tag_type tag, uint64_t value) { + add_tagged_varint(tag, value); + } + + /** + * Add "fixed32" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_fixed32(pbf_tag_type tag, uint32_t value) { + add_field(tag, pbf_wire_type::fixed32); + add_fixed(value); + } + + /** + * Add "sfixed32" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_sfixed32(pbf_tag_type tag, int32_t value) { + add_field(tag, pbf_wire_type::fixed32); + add_fixed(value); + } + + /** + * Add "fixed64" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_fixed64(pbf_tag_type tag, uint64_t value) { + add_field(tag, pbf_wire_type::fixed64); + add_fixed(value); + } + + /** + * Add "sfixed64" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_sfixed64(pbf_tag_type tag, int64_t value) { + add_field(tag, pbf_wire_type::fixed64); + add_fixed(value); + } + + /** + * Add "float" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_float(pbf_tag_type tag, float value) { + add_field(tag, pbf_wire_type::fixed32); + add_fixed(value); + } + + /** + * Add "double" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_double(pbf_tag_type tag, double value) { + add_field(tag, pbf_wire_type::fixed64); + add_fixed(value); + } + + /** + * Add "bytes" field to data. + * + * @param tag Tag (field number) of the field + * @param value Pointer to value to be written + * @param size Number of bytes to be written + */ + void add_bytes(pbf_tag_type tag, const char* value, std::size_t size) { + protozero_assert(m_pos == 0 && "you can't add fields to a parent basic_pbf_writer if there is an existing basic_pbf_writer for a submessage"); + protozero_assert(m_data); + protozero_assert(size <= std::numeric_limits::max()); + add_length_varint(tag, pbf_length_type(size)); + buffer_customization::append(m_data, value, size); + } + + /** + * Add "bytes" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_bytes(pbf_tag_type tag, const data_view& value) { + add_bytes(tag, value.data(), value.size()); + } + + /** + * Add "bytes" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_bytes(pbf_tag_type tag, const std::string& value) { + add_bytes(tag, value.data(), value.size()); + } + + /** + * Add "bytes" field to data. Bytes from the value are written until + * a null byte is encountered. The null byte is not added. + * + * @param tag Tag (field number) of the field + * @param value Pointer to zero-delimited value to be written + */ + void add_bytes(pbf_tag_type tag, const char* value) { + add_bytes(tag, value, std::strlen(value)); + } + + /** + * Add "bytes" field to data using vectored input. All the data in the + * 2nd and further arguments is "concatenated" with only a single copy + * into the final buffer. + * + * This will work with objects of any type supporting the data() and + * size() methods like std::string or protozero::data_view. + * + * Example: + * @code + * std::string data1 = "abc"; + * std::string data2 = "xyz"; + * writer.add_bytes_vectored(1, data1, data2); + * @endcode + * + * @tparam Ts List of types supporting data() and size() methods. + * @param tag Tag (field number) of the field + * @param values List of objects of types Ts with data to be appended. + */ + template + void add_bytes_vectored(pbf_tag_type tag, Ts&&... values) { + protozero_assert(m_pos == 0 && "you can't add fields to a parent basic_pbf_writer if there is an existing basic_pbf_writer for a submessage"); + protozero_assert(m_data); + size_t sum_size = 0; + (void)std::initializer_list{sum_size += values.size()...}; + protozero_assert(sum_size <= std::numeric_limits::max()); + add_length_varint(tag, pbf_length_type(sum_size)); + buffer_customization::reserve_additional(m_data, sum_size); + (void)std::initializer_list{(buffer_customization::append(m_data, values.data(), values.size()), 0)...}; + } + + /** + * Add "string" field to data. + * + * @param tag Tag (field number) of the field + * @param value Pointer to value to be written + * @param size Number of bytes to be written + */ + void add_string(pbf_tag_type tag, const char* value, std::size_t size) { + add_bytes(tag, value, size); + } + + /** + * Add "string" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_string(pbf_tag_type tag, const data_view& value) { + add_bytes(tag, value.data(), value.size()); + } + + /** + * Add "string" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written + */ + void add_string(pbf_tag_type tag, const std::string& value) { + add_bytes(tag, value.data(), value.size()); + } + + /** + * Add "string" field to data. Bytes from the value are written until + * a null byte is encountered. The null byte is not added. + * + * @param tag Tag (field number) of the field + * @param value Pointer to value to be written + */ + void add_string(pbf_tag_type tag, const char* value) { + add_bytes(tag, value, std::strlen(value)); + } + + /** + * Add "message" field to data. + * + * @param tag Tag (field number) of the field + * @param value Pointer to message to be written + * @param size Length of the message + */ + void add_message(pbf_tag_type tag, const char* value, std::size_t size) { + add_bytes(tag, value, size); + } + + /** + * Add "message" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written. The value must be a complete message. + */ + void add_message(pbf_tag_type tag, const data_view& value) { + add_bytes(tag, value.data(), value.size()); + } + + /** + * Add "message" field to data. + * + * @param tag Tag (field number) of the field + * @param value Value to be written. The value must be a complete message. + */ + void add_message(pbf_tag_type tag, const std::string& value) { + add_bytes(tag, value.data(), value.size()); + } + + ///@} + + ///@{ + /** + * @name Repeated packed field writer functions + */ + + /** + * Add "repeated packed bool" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to bool. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_bool(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_varint(tag, first, last); + } + + /** + * Add "repeated packed enum" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to int32_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_enum(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_varint(tag, first, last); + } + + /** + * Add "repeated packed int32" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to int32_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_int32(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_varint(tag, first, last); + } + + /** + * Add "repeated packed sint32" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to int32_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_sint32(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_svarint(tag, first, last); + } + + /** + * Add "repeated packed uint32" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to uint32_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_uint32(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_varint(tag, first, last); + } + + /** + * Add "repeated packed int64" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to int64_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_int64(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_varint(tag, first, last); + } + + /** + * Add "repeated packed sint64" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to int64_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_sint64(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_svarint(tag, first, last); + } + + /** + * Add "repeated packed uint64" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to uint64_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_uint64(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_varint(tag, first, last); + } + + /** + * Add a "repeated packed" fixed-size field to data. The following + * fixed-size fields are available: + * + * uint32_t -> repeated packed fixed32 + * int32_t -> repeated packed sfixed32 + * uint64_t -> repeated packed fixed64 + * int64_t -> repeated packed sfixed64 + * double -> repeated packed double + * float -> repeated packed float + * + * @tparam ValueType One of the following types: (u)int32/64_t, double, float. + * @tparam InputIterator A type satisfying the InputIterator concept. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_fixed(pbf_tag_type tag, InputIterator first, InputIterator last) { + static_assert(std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value, "Only some types are allowed"); + add_packed_fixed(tag, first, last, + typename std::iterator_traits::iterator_category{}); + } + + /** + * Add "repeated packed fixed32" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to uint32_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_fixed32(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_fixed(tag, first, last, + typename std::iterator_traits::iterator_category{}); + } + + /** + * Add "repeated packed sfixed32" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to int32_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_sfixed32(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_fixed(tag, first, last, + typename std::iterator_traits::iterator_category{}); + } + + /** + * Add "repeated packed fixed64" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to uint64_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_fixed64(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_fixed(tag, first, last, + typename std::iterator_traits::iterator_category{}); + } + + /** + * Add "repeated packed sfixed64" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to int64_t. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_sfixed64(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_fixed(tag, first, last, + typename std::iterator_traits::iterator_category{}); + } + + /** + * Add "repeated packed float" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to float. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_float(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_fixed(tag, first, last, + typename std::iterator_traits::iterator_category{}); + } + + /** + * Add "repeated packed double" field to data. + * + * @tparam InputIterator A type satisfying the InputIterator concept. + * Dereferencing the iterator must yield a type assignable to double. + * @param tag Tag (field number) of the field + * @param first Iterator pointing to the beginning of the data + * @param last Iterator pointing one past the end of data + */ + template + void add_packed_double(pbf_tag_type tag, InputIterator first, InputIterator last) { + add_packed_fixed(tag, first, last, + typename std::iterator_traits::iterator_category{}); + } + + ///@} + + template friend class detail::packed_field_varint; + template friend class detail::packed_field_svarint; + template friend class detail::packed_field_fixed; + +}; // class basic_pbf_writer + +/** + * Swap two basic_pbf_writer objects. + * + * @param lhs First object. + * @param rhs Second object. + */ +template +inline void swap(basic_pbf_writer& lhs, basic_pbf_writer& rhs) noexcept { + lhs.swap(rhs); +} + +namespace detail { + + template + class packed_field { + + basic_pbf_writer m_writer{}; + + public: + + packed_field(const packed_field&) = delete; + packed_field& operator=(const packed_field&) = delete; + + packed_field(packed_field&&) noexcept = default; + packed_field& operator=(packed_field&&) noexcept = default; + + packed_field() = default; + + packed_field(basic_pbf_writer& parent_writer, pbf_tag_type tag) : + m_writer{parent_writer, tag} { + } + + packed_field(basic_pbf_writer& parent_writer, pbf_tag_type tag, std::size_t size) : + m_writer{parent_writer, tag, size} { + } + + ~packed_field() noexcept = default; + + bool valid() const noexcept { + return m_writer.valid(); + } + + void commit() { + m_writer.commit(); + } + + void rollback() { + m_writer.rollback(); + } + + basic_pbf_writer& writer() noexcept { + return m_writer; + } + + }; // class packed_field + + template + class packed_field_fixed : public packed_field { + + public: + + packed_field_fixed() : + packed_field{} { + } + + template + packed_field_fixed(basic_pbf_writer& parent_writer, P tag) : + packed_field{parent_writer, static_cast(tag)} { + } + + template + packed_field_fixed(basic_pbf_writer& parent_writer, P tag, std::size_t size) : + packed_field{parent_writer, static_cast(tag), size * sizeof(T)} { + } + + void add_element(T value) { + this->writer().template add_fixed(value); + } + + }; // class packed_field_fixed + + template + class packed_field_varint : public packed_field { + + public: + + packed_field_varint() : + packed_field{} { + } + + template + packed_field_varint(basic_pbf_writer& parent_writer, P tag) : + packed_field{parent_writer, static_cast(tag)} { + } + + void add_element(T value) { + this->writer().add_varint(uint64_t(value)); + } + + }; // class packed_field_varint + + template + class packed_field_svarint : public packed_field { + + public: + + packed_field_svarint() : + packed_field{} { + } + + template + packed_field_svarint(basic_pbf_writer& parent_writer, P tag) : + packed_field{parent_writer, static_cast(tag)} { + } + + void add_element(T value) { + this->writer().add_varint(encode_zigzag64(value)); + } + + }; // class packed_field_svarint + +} // end namespace detail + +} // end namespace protozero + +#endif // PROTOZERO_BASIC_PBF_WRITER_HPP diff --git a/include/protozero/buffer_fixed.hpp b/include/protozero/buffer_fixed.hpp new file mode 100644 index 00000000..b2e6d1d2 --- /dev/null +++ b/include/protozero/buffer_fixed.hpp @@ -0,0 +1,222 @@ +#ifndef PROTOZERO_BUFFER_FIXED_HPP +#define PROTOZERO_BUFFER_FIXED_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file buffer_fixed.hpp + * + * @brief Contains the fixed_size_buffer_adaptor class. + */ + +#include "buffer_tmpl.hpp" +#include "config.hpp" + +#include +#include +#include +#include + +namespace protozero { + +/** + * This class can be used instead of std::string if you want to create a + * vector tile in a fixed-size buffer. Any operation that needs more space + * than is available will fail with a std::length_error exception. + */ +class fixed_size_buffer_adaptor { + + char* m_data; + std::size_t m_capacity; + std::size_t m_size = 0; + +public: + + /// @cond usual container typedefs not documented + + using size_type = std::size_t; + + using value_type = char; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = value_type*; + using const_pointer = const value_type*; + + using iterator = pointer; + using const_iterator = const_pointer; + + /// @endcond + + /** + * Constructor. + * + * @param data Pointer to some memory allocated for the buffer. + * @param capacity Number of bytes available. + */ + fixed_size_buffer_adaptor(char* data, std::size_t capacity) noexcept : + m_data(data), + m_capacity(capacity) { + } + + /** + * Constructor. + * + * @param container Some container class supporting the member functions + * data() and size(). + */ + template + explicit fixed_size_buffer_adaptor(T& container) : + m_data(container.data()), + m_capacity(container.size()) { + } + + /// Returns a pointer to the data in the buffer. + const char* data() const noexcept { + return m_data; + } + + /// Returns a pointer to the data in the buffer. + char* data() noexcept { + return m_data; + } + + /// The capacity this buffer was created with. + std::size_t capacity() const noexcept { + return m_capacity; + } + + /// The number of bytes used in the buffer. Always <= capacity(). + std::size_t size() const noexcept { + return m_size; + } + + /// Return iterator to beginning of data. + char* begin() noexcept { + return m_data; + } + + /// Return iterator to beginning of data. + const char* begin() const noexcept { + return m_data; + } + + /// Return iterator to beginning of data. + const char* cbegin() const noexcept { + return m_data; + } + + /// Return iterator to end of data. + char* end() noexcept { + return m_data + m_size; + } + + /// Return iterator to end of data. + const char* end() const noexcept { + return m_data + m_size; + } + + /// Return iterator to end of data. + const char* cend() const noexcept { + return m_data + m_size; + } + +/// @cond INTERNAL + + // Do not rely on anything beyond this point + + void append(const char* data, std::size_t count) { + if (m_size + count > m_capacity) { + throw std::length_error{"fixed size data store exhausted"}; + } + std::copy_n(data, count, m_data + m_size); + m_size += count; + } + + void append_zeros(std::size_t count) { + if (m_size + count > m_capacity) { + throw std::length_error{"fixed size data store exhausted"}; + } + std::fill_n(m_data + m_size, count, '\0'); + m_size += count; + } + + void resize(std::size_t size) { + protozero_assert(size < m_size); + if (size > m_capacity) { + throw std::length_error{"fixed size data store exhausted"}; + } + m_size = size; + } + + void erase_range(std::size_t from, std::size_t to) { + protozero_assert(from <= m_size); + protozero_assert(to <= m_size); + protozero_assert(from < to); + std::copy(m_data + to, m_data + m_size, m_data + from); + m_size -= (to - from); + } + + char* at_pos(std::size_t pos) { + protozero_assert(pos <= m_size); + return m_data + pos; + } + + void push_back(char ch) { + if (m_size >= m_capacity) { + throw std::length_error{"fixed size data store exhausted"}; + } + m_data[m_size++] = ch; + } +/// @endcond + +}; // class fixed_size_buffer_adaptor + +/// @cond INTERNAL +template <> +struct buffer_customization { + + static std::size_t size(const fixed_size_buffer_adaptor* buffer) noexcept { + return buffer->size(); + } + + static void append(fixed_size_buffer_adaptor* buffer, const char* data, std::size_t count) { + buffer->append(data, count); + } + + static void append_zeros(fixed_size_buffer_adaptor* buffer, std::size_t count) { + buffer->append_zeros(count); + } + + static void resize(fixed_size_buffer_adaptor* buffer, std::size_t size) { + buffer->resize(size); + } + + static void reserve_additional(fixed_size_buffer_adaptor* /*buffer*/, std::size_t /*size*/) { + /* nothing to be done for fixed-size buffers */ + } + + static void erase_range(fixed_size_buffer_adaptor* buffer, std::size_t from, std::size_t to) { + buffer->erase_range(from, to); + } + + static char* at_pos(fixed_size_buffer_adaptor* buffer, std::size_t pos) { + return buffer->at_pos(pos); + } + + static void push_back(fixed_size_buffer_adaptor* buffer, char ch) { + buffer->push_back(ch); + } + +}; +/// @endcond + +} // namespace protozero + +#endif // PROTOZERO_BUFFER_FIXED_HPP diff --git a/include/protozero/buffer_string.hpp b/include/protozero/buffer_string.hpp new file mode 100644 index 00000000..02e8ad25 --- /dev/null +++ b/include/protozero/buffer_string.hpp @@ -0,0 +1,78 @@ +#ifndef PROTOZERO_BUFFER_STRING_HPP +#define PROTOZERO_BUFFER_STRING_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file buffer_string.hpp + * + * @brief Contains the customization points for buffer implementation based + * on std::string + */ + +#include "buffer_tmpl.hpp" +#include "config.hpp" + +#include +#include +#include + +namespace protozero { + +// Implementation of buffer customizations points for std::string + +/// @cond INTERNAL +template <> +struct buffer_customization { + + static std::size_t size(const std::string* buffer) noexcept { + return buffer->size(); + } + + static void append(std::string* buffer, const char* data, std::size_t count) { + buffer->append(data, count); + } + + static void append_zeros(std::string* buffer, std::size_t count) { + buffer->append(count, '\0'); + } + + static void resize(std::string* buffer, std::size_t size) { + protozero_assert(size < buffer->size()); + buffer->resize(size); + } + + static void reserve_additional(std::string* buffer, std::size_t size) { + buffer->reserve(buffer->size() + size); + } + + static void erase_range(std::string* buffer, std::size_t from, std::size_t to) { + protozero_assert(from <= buffer->size()); + protozero_assert(to <= buffer->size()); + protozero_assert(from <= to); + buffer->erase(std::next(buffer->begin(), static_cast(from)), + std::next(buffer->begin(), static_cast(to))); + } + + static char* at_pos(std::string* buffer, std::size_t pos) { + protozero_assert(pos <= buffer->size()); + return (&*buffer->begin()) + pos; + } + + static void push_back(std::string* buffer, char ch) { + buffer->push_back(ch); + } + +}; +/// @endcond + +} // namespace protozero + +#endif // PROTOZERO_BUFFER_STRING_HPP diff --git a/include/protozero/buffer_tmpl.hpp b/include/protozero/buffer_tmpl.hpp new file mode 100644 index 00000000..ac223996 --- /dev/null +++ b/include/protozero/buffer_tmpl.hpp @@ -0,0 +1,113 @@ +#ifndef PROTOZERO_BUFFER_TMPL_HPP +#define PROTOZERO_BUFFER_TMPL_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file buffer_tmpl.hpp + * + * @brief Contains the customization points for buffer implementations. + */ + +#include +#include +#include + +namespace protozero { + +// Implementation of buffer customizations points for std::string + +/// @cond INTERNAL +template +struct buffer_customization { + + /** + * Get the number of bytes currently used in the buffer. + * + * @param buffer Pointer to the buffer. + * @returns number of bytes used in the buffer. + */ + static std::size_t size(const std::string* buffer); + + /** + * Append count bytes from data to the buffer. + * + * @param buffer Pointer to the buffer. + * @param data Pointer to the data. + * @param count Number of bytes to be added to the buffer. + */ + static void append(std::string* buffer, const char* data, std::size_t count); + + /** + * Append count zero bytes to the buffer. + * + * @param buffer Pointer to the buffer. + * @param count Number of bytes to be added to the buffer. + */ + static void append_zeros(std::string* buffer, std::size_t count); + + /** + * Shrink the buffer to the specified size. The new size will always be + * smaller than the current size. + * + * @param buffer Pointer to the buffer. + * @param size New size of the buffer. + * + * @pre size < current size of buffer + */ + static void resize(std::string* buffer, std::size_t size); + + /** + * Reserve an additional size bytes for use in the buffer. This is used for + * variable-sized buffers to tell the buffer implementation that soon more + * memory will be used. The implementation can ignore this. + * + * @param buffer Pointer to the buffer. + * @param size Number of bytes to reserve. + */ + static void reserve_additional(std::string* buffer, std::size_t size); + + /** + * Delete data from the buffer. This must move back the data after the + * part being deleted and resize the buffer accordingly. + * + * @param buffer Pointer to the buffer. + * @param from Offset into the buffer where we want to erase from. + * @param to Offset into the buffer one past the last byte we want to erase. + * + * @pre from, to <= size of the buffer, from < to + */ + static void erase_range(std::string* buffer, std::size_t from, std::size_t to); + + /** + * Return a pointer to the memory at the specified position in the buffer. + * + * @param buffer Pointer to the buffer. + * @param pos The position in the buffer. + * @returns pointer to the memory in the buffer at the specified position. + * + * @pre pos <= size of the buffer + */ + static char* at_pos(std::string* buffer, std::size_t pos); + + /** + * Add a char to the buffer incrementing the number of chars in the buffer. + * + * @param buffer Pointer to the buffer. + * @param ch The character to add. + */ + static void push_back(std::string* buffer, char ch); + +}; +/// @endcond + +} // namespace protozero + +#endif // PROTOZERO_BUFFER_TMPL_HPP diff --git a/include/protozero/buffer_vector.hpp b/include/protozero/buffer_vector.hpp new file mode 100644 index 00000000..c163300c --- /dev/null +++ b/include/protozero/buffer_vector.hpp @@ -0,0 +1,78 @@ +#ifndef PROTOZERO_BUFFER_VECTOR_HPP +#define PROTOZERO_BUFFER_VECTOR_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file buffer_vector.hpp + * + * @brief Contains the customization points for buffer implementation based + * on std::vector + */ + +#include "buffer_tmpl.hpp" +#include "config.hpp" + +#include +#include +#include + +namespace protozero { + +// Implementation of buffer customizations points for std::vector + +/// @cond INTERNAL +template <> +struct buffer_customization> { + + static std::size_t size(const std::vector* buffer) noexcept { + return buffer->size(); + } + + static void append(std::vector* buffer, const char* data, std::size_t count) { + buffer->insert(buffer->end(), data, data + count); + } + + static void append_zeros(std::vector* buffer, std::size_t count) { + buffer->insert(buffer->end(), count, '\0'); + } + + static void resize(std::vector* buffer, std::size_t size) { + protozero_assert(size < buffer->size()); + buffer->resize(size); + } + + static void reserve_additional(std::vector* buffer, std::size_t size) { + buffer->reserve(buffer->size() + size); + } + + static void erase_range(std::vector* buffer, std::size_t from, std::size_t to) { + protozero_assert(from <= buffer->size()); + protozero_assert(to <= buffer->size()); + protozero_assert(from <= to); + buffer->erase(std::next(buffer->begin(), static_cast(from)), + std::next(buffer->begin(), static_cast(to))); + } + + static char* at_pos(std::vector* buffer, std::size_t pos) { + protozero_assert(pos <= buffer->size()); + return (&*buffer->begin()) + pos; + } + + static void push_back(std::vector* buffer, char ch) { + buffer->push_back(ch); + } + +}; +/// @endcond + +} // namespace protozero + +#endif // PROTOZERO_BUFFER_VECTOR_HPP diff --git a/include/protozero/byteswap.hpp b/include/protozero/byteswap.hpp new file mode 100644 index 00000000..75cae691 --- /dev/null +++ b/include/protozero/byteswap.hpp @@ -0,0 +1,108 @@ +#ifndef PROTOZERO_BYTESWAP_HPP +#define PROTOZERO_BYTESWAP_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file byteswap.hpp + * + * @brief Contains functions to swap bytes in values (for different endianness). + */ + +#include "config.hpp" + +#include +#include + +namespace protozero { +namespace detail { + +inline uint32_t byteswap_impl(uint32_t value) noexcept { +#ifdef PROTOZERO_USE_BUILTIN_BSWAP + return __builtin_bswap32(value); +#else + return ((value & 0xff000000U) >> 24U) | + ((value & 0x00ff0000U) >> 8U) | + ((value & 0x0000ff00U) << 8U) | + ((value & 0x000000ffU) << 24U); +#endif +} + +inline uint64_t byteswap_impl(uint64_t value) noexcept { +#ifdef PROTOZERO_USE_BUILTIN_BSWAP + return __builtin_bswap64(value); +#else + return ((value & 0xff00000000000000ULL) >> 56U) | + ((value & 0x00ff000000000000ULL) >> 40U) | + ((value & 0x0000ff0000000000ULL) >> 24U) | + ((value & 0x000000ff00000000ULL) >> 8U) | + ((value & 0x00000000ff000000ULL) << 8U) | + ((value & 0x0000000000ff0000ULL) << 24U) | + ((value & 0x000000000000ff00ULL) << 40U) | + ((value & 0x00000000000000ffULL) << 56U); +#endif +} + +} // end namespace detail + +/// byteswap the data pointed to by ptr in-place. +inline void byteswap_inplace(uint32_t* ptr) noexcept { + *ptr = detail::byteswap_impl(*ptr); +} + +/// byteswap the data pointed to by ptr in-place. +inline void byteswap_inplace(uint64_t* ptr) noexcept { + *ptr = detail::byteswap_impl(*ptr); +} + +/// byteswap the data pointed to by ptr in-place. +inline void byteswap_inplace(int32_t* ptr) noexcept { + auto* bptr = reinterpret_cast(ptr); + *bptr = detail::byteswap_impl(*bptr); +} + +/// byteswap the data pointed to by ptr in-place. +inline void byteswap_inplace(int64_t* ptr) noexcept { + auto* bptr = reinterpret_cast(ptr); + *bptr = detail::byteswap_impl(*bptr); +} + +/// byteswap the data pointed to by ptr in-place. +inline void byteswap_inplace(float* ptr) noexcept { + static_assert(sizeof(float) == 4, "Expecting four byte float"); + + uint32_t tmp = 0; + std::memcpy(&tmp, ptr, 4); + tmp = detail::byteswap_impl(tmp); // uint32 overload + std::memcpy(ptr, &tmp, 4); +} + +/// byteswap the data pointed to by ptr in-place. +inline void byteswap_inplace(double* ptr) noexcept { + static_assert(sizeof(double) == 8, "Expecting eight byte double"); + + uint64_t tmp = 0; + std::memcpy(&tmp, ptr, 8); + tmp = detail::byteswap_impl(tmp); // uint64 overload + std::memcpy(ptr, &tmp, 8); +} + +namespace detail { + + // Added for backwards compatibility with any code that might use this + // function (even if it shouldn't have). Will be removed in a later + // version of protozero. + using ::protozero::byteswap_inplace; + +} // end namespace detail + +} // end namespace protozero + +#endif // PROTOZERO_BYTESWAP_HPP diff --git a/include/protozero/config.hpp b/include/protozero/config.hpp new file mode 100644 index 00000000..6fc77490 --- /dev/null +++ b/include/protozero/config.hpp @@ -0,0 +1,48 @@ +#ifndef PROTOZERO_CONFIG_HPP +#define PROTOZERO_CONFIG_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +#include + +/** + * @file config.hpp + * + * @brief Contains macro checks for different configurations. + */ + +#define PROTOZERO_LITTLE_ENDIAN 1234 +#define PROTOZERO_BIG_ENDIAN 4321 + +// Find out which byte order the machine has. +#if defined(__BYTE_ORDER) +# if (__BYTE_ORDER == __LITTLE_ENDIAN) +# define PROTOZERO_BYTE_ORDER PROTOZERO_LITTLE_ENDIAN +# endif +# if (__BYTE_ORDER == __BIG_ENDIAN) +# define PROTOZERO_BYTE_ORDER PROTOZERO_BIG_ENDIAN +# endif +#else +// This probably isn't a very good default, but might do until we figure +// out something better. +# define PROTOZERO_BYTE_ORDER PROTOZERO_LITTLE_ENDIAN +#endif + +// Check whether __builtin_bswap is available +#if defined(__GNUC__) || defined(__clang__) +# define PROTOZERO_USE_BUILTIN_BSWAP +#endif + +// Wrapper for assert() used for testing +#ifndef protozero_assert +# define protozero_assert(x) assert(x) +#endif + +#endif // PROTOZERO_CONFIG_HPP diff --git a/include/protozero/data_view.hpp b/include/protozero/data_view.hpp new file mode 100644 index 00000000..3ec87af3 --- /dev/null +++ b/include/protozero/data_view.hpp @@ -0,0 +1,236 @@ +#ifndef PROTOZERO_DATA_VIEW_HPP +#define PROTOZERO_DATA_VIEW_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file data_view.hpp + * + * @brief Contains the implementation of the data_view class. + */ + +#include "config.hpp" + +#include +#include +#include +#include +#include + +namespace protozero { + +#ifdef PROTOZERO_USE_VIEW +using data_view = PROTOZERO_USE_VIEW; +#else + +/** + * Holds a pointer to some data and a length. + * + * This class is supposed to be compatible with the std::string_view + * that will be available in C++17. + */ +class data_view { + + const char* m_data = nullptr; + std::size_t m_size = 0; + +public: + + /** + * Default constructor. Construct an empty data_view. + */ + constexpr data_view() noexcept = default; + + /** + * Create data_view from pointer and size. + * + * @param ptr Pointer to the data. + * @param length Length of the data. + */ + constexpr data_view(const char* ptr, std::size_t length) noexcept + : m_data{ptr}, + m_size{length} { + } + + /** + * Create data_view from string. + * + * @param str String with the data. + */ + data_view(const std::string& str) noexcept // NOLINT(google-explicit-constructor, hicpp-explicit-conversions) + : m_data{str.data()}, + m_size{str.size()} { + } + + /** + * Create data_view from zero-terminated string. + * + * @param ptr Pointer to the data. + */ + data_view(const char* ptr) noexcept // NOLINT(google-explicit-constructor, hicpp-explicit-conversions) + : m_data{ptr}, + m_size{std::strlen(ptr)} { + } + + /** + * Swap the contents of this object with the other. + * + * @param other Other object to swap data with. + */ + void swap(data_view& other) noexcept { + using std::swap; + swap(m_data, other.m_data); + swap(m_size, other.m_size); + } + + /// Return pointer to data. + constexpr const char* data() const noexcept { + return m_data; + } + + /// Return length of data in bytes. + constexpr std::size_t size() const noexcept { + return m_size; + } + + /// Returns true if size is 0. + constexpr bool empty() const noexcept { + return m_size == 0; + } + +#ifndef PROTOZERO_STRICT_API + /** + * Convert data view to string. + * + * @pre Must not be default constructed data_view. + * + * @deprecated to_string() is not available in C++17 string_view so it + * should not be used to make conversion to that class easier + * in the future. + */ + std::string to_string() const { + protozero_assert(m_data); + return {m_data, m_size}; + } +#endif + + /** + * Convert data view to string. + * + * @pre Must not be default constructed data_view. + */ + explicit operator std::string() const { + protozero_assert(m_data); + return {m_data, m_size}; + } + + /** + * Compares the contents of this object with the given other object. + * + * @returns 0 if they are the same, <0 if this object is smaller than + * the other or >0 if it is larger. If both objects have the + * same size returns <0 if this object is lexicographically + * before the other, >0 otherwise. + * + * @pre Must not be default constructed data_view. + */ + int compare(data_view other) const noexcept { + assert(m_data && other.m_data); + const int cmp = std::memcmp(data(), other.data(), + std::min(size(), other.size())); + if (cmp == 0) { + if (size() == other.size()) { + return 0; + } + return size() < other.size() ? -1 : 1; + } + return cmp; + } + +}; // class data_view + +/** + * Swap two data_view objects. + * + * @param lhs First object. + * @param rhs Second object. + */ +inline void swap(data_view& lhs, data_view& rhs) noexcept { + lhs.swap(rhs); +} + +/** + * Two data_view instances are equal if they have the same size and the + * same content. + * + * @param lhs First object. + * @param rhs Second object. + */ +inline constexpr bool operator==(const data_view lhs, const data_view rhs) noexcept { + return lhs.size() == rhs.size() && + std::equal(lhs.data(), lhs.data() + lhs.size(), rhs.data()); +} + +/** + * Two data_view instances are not equal if they have different sizes or the + * content differs. + * + * @param lhs First object. + * @param rhs Second object. + */ +inline constexpr bool operator!=(const data_view lhs, const data_view rhs) noexcept { + return !(lhs == rhs); +} + +/** + * Returns true if lhs.compare(rhs) < 0. + * + * @param lhs First object. + * @param rhs Second object. + */ +inline bool operator<(const data_view lhs, const data_view rhs) noexcept { + return lhs.compare(rhs) < 0; +} + +/** + * Returns true if lhs.compare(rhs) <= 0. + * + * @param lhs First object. + * @param rhs Second object. + */ +inline bool operator<=(const data_view lhs, const data_view rhs) noexcept { + return lhs.compare(rhs) <= 0; +} + +/** + * Returns true if lhs.compare(rhs) > 0. + * + * @param lhs First object. + * @param rhs Second object. + */ +inline bool operator>(const data_view lhs, const data_view rhs) noexcept { + return lhs.compare(rhs) > 0; +} + +/** + * Returns true if lhs.compare(rhs) >= 0. + * + * @param lhs First object. + * @param rhs Second object. + */ +inline bool operator>=(const data_view lhs, const data_view rhs) noexcept { + return lhs.compare(rhs) >= 0; +} + +#endif + +} // end namespace protozero + +#endif // PROTOZERO_DATA_VIEW_HPP diff --git a/include/protozero/exception.hpp b/include/protozero/exception.hpp new file mode 100644 index 00000000..a3cd0f15 --- /dev/null +++ b/include/protozero/exception.hpp @@ -0,0 +1,101 @@ +#ifndef PROTOZERO_EXCEPTION_HPP +#define PROTOZERO_EXCEPTION_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file exception.hpp + * + * @brief Contains the exceptions used in the protozero library. + */ + +#include + +/** + * @brief All parts of the protozero header-only library are in this namespace. + */ +namespace protozero { + +/** + * All exceptions explicitly thrown by the functions of the protozero library + * derive from this exception. + */ +struct exception : std::exception { + /// Returns the explanatory string. + const char* what() const noexcept override { + return "pbf exception"; + } +}; + +/** + * This exception is thrown when parsing a varint thats larger than allowed. + * This should never happen unless the data is corrupted. + */ +struct varint_too_long_exception : exception { + /// Returns the explanatory string. + const char* what() const noexcept override { + return "varint too long exception"; + } +}; + +/** + * This exception is thrown when the wire type of a pdf field is unknown. + * This should never happen unless the data is corrupted. + */ +struct unknown_pbf_wire_type_exception : exception { + /// Returns the explanatory string. + const char* what() const noexcept override { + return "unknown pbf field type exception"; + } +}; + +/** + * This exception is thrown when we are trying to read a field and there + * are not enough bytes left in the buffer to read it. Almost all functions + * of the pbf_reader class can throw this exception. + * + * This should never happen unless the data is corrupted or you have + * initialized the pbf_reader object with incomplete data. + */ +struct end_of_buffer_exception : exception { + /// Returns the explanatory string. + const char* what() const noexcept override { + return "end of buffer exception"; + } +}; + +/** + * This exception is thrown when a tag has an invalid value. Tags must be + * unsigned integers between 1 and 2^29-1. Tags between 19000 and 19999 are + * not allowed. See + * https://developers.google.com/protocol-buffers/docs/proto#assigning-tags + */ +struct invalid_tag_exception : exception { + /// Returns the explanatory string. + const char* what() const noexcept override { + return "invalid tag exception"; + } +}; + +/** + * This exception is thrown when a length field of a packed repeated field is + * invalid. For fixed size types the length must be a multiple of the size of + * the type. + */ +struct invalid_length_exception : exception { + /// Returns the explanatory string. + const char* what() const noexcept override { + return "invalid length exception"; + } +}; + +} // end namespace protozero + +#endif // PROTOZERO_EXCEPTION_HPP diff --git a/include/protozero/iterators.hpp b/include/protozero/iterators.hpp new file mode 100644 index 00000000..ee8ef8ec --- /dev/null +++ b/include/protozero/iterators.hpp @@ -0,0 +1,481 @@ +#ifndef PROTOZERO_ITERATORS_HPP +#define PROTOZERO_ITERATORS_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file iterators.hpp + * + * @brief Contains the iterators for access to packed repeated fields. + */ + +#include "config.hpp" +#include "varint.hpp" + +#if PROTOZERO_BYTE_ORDER != PROTOZERO_LITTLE_ENDIAN +# include +#endif + +#include +#include +#include +#include + +namespace protozero { + +/** + * A range of iterators based on std::pair. Created from beginning and + * end iterators. Used as a return type from some pbf_reader methods + * that is easy to use with range-based for loops. + */ +template > +class iterator_range : +#ifdef PROTOZERO_STRICT_API + protected +#else + public +#endif + P { + +public: + + /// The type of the iterators in this range. + using iterator = T; + + /// The value type of the underlying iterator. + using value_type = typename std::iterator_traits::value_type; + + /** + * Default constructor. Create empty iterator_range. + */ + constexpr iterator_range() : + P{iterator{}, iterator{}} { + } + + /** + * Create iterator range from two iterators. + * + * @param first_iterator Iterator to beginning of range. + * @param last_iterator Iterator to end of range. + */ + constexpr iterator_range(iterator&& first_iterator, iterator&& last_iterator) : + P{std::forward(first_iterator), + std::forward(last_iterator)} { + } + + /// Return iterator to beginning of range. + constexpr iterator begin() const noexcept { + return this->first; + } + + /// Return iterator to end of range. + constexpr iterator end() const noexcept { + return this->second; + } + + /// Return iterator to beginning of range. + constexpr iterator cbegin() const noexcept { + return this->first; + } + + /// Return iterator to end of range. + constexpr iterator cend() const noexcept { + return this->second; + } + + /** + * Return true if this range is empty. + * + * Complexity: Constant. + */ + constexpr bool empty() const noexcept { + return begin() == end(); + } + + /** + * Get the size of the range, ie the number of elements it contains. + * + * Complexity: Constant or linear depending on the underlaying iterator. + */ + std::size_t size() const noexcept { + return static_cast(std::distance(begin(), end())); + } + + /** + * Get element at the beginning of the range. + * + * @pre Range must not be empty. + */ + value_type front() const { + protozero_assert(!empty()); + return *(this->first); + } + + /** + * Advance beginning of range by one. + * + * @pre Range must not be empty. + */ + void drop_front() { + protozero_assert(!empty()); + ++this->first; + } + + /** + * Swap the contents of this range with the other. + * + * @param other Other range to swap data with. + */ + void swap(iterator_range& other) noexcept { + using std::swap; + swap(this->first, other.first); + swap(this->second, other.second); + } + +}; // struct iterator_range + +/** + * Swap two iterator_ranges. + * + * @param lhs First range. + * @param rhs Second range. + */ +template +inline void swap(iterator_range& lhs, iterator_range& rhs) noexcept { + lhs.swap(rhs); +} + +/** + * A forward iterator used for accessing packed repeated fields of fixed + * length (fixed32, sfixed32, float, double). + */ +template +class const_fixed_iterator { + + /// Pointer to current iterator position + const char* m_data = nullptr; + +public: + + /// @cond usual iterator functions not documented + + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + + const_fixed_iterator() noexcept = default; + + explicit const_fixed_iterator(const char* data) noexcept : + m_data{data} { + } + + const_fixed_iterator(const const_fixed_iterator&) noexcept = default; + const_fixed_iterator(const_fixed_iterator&&) noexcept = default; + + const_fixed_iterator& operator=(const const_fixed_iterator&) noexcept = default; + const_fixed_iterator& operator=(const_fixed_iterator&&) noexcept = default; + + ~const_fixed_iterator() noexcept = default; + + value_type operator*() const noexcept { + value_type result; + std::memcpy(&result, m_data, sizeof(value_type)); +#if PROTOZERO_BYTE_ORDER != PROTOZERO_LITTLE_ENDIAN + byteswap_inplace(&result); +#endif + return result; + } + + const_fixed_iterator& operator++() noexcept { + m_data += sizeof(value_type); + return *this; + } + + const_fixed_iterator operator++(int) noexcept { + const const_fixed_iterator tmp{*this}; + ++(*this); + return tmp; + } + + const_fixed_iterator& operator--() noexcept { + m_data -= sizeof(value_type); + return *this; + } + + const_fixed_iterator operator--(int) noexcept { + const const_fixed_iterator tmp{*this}; + --(*this); + return tmp; + } + + friend bool operator==(const_fixed_iterator lhs, const_fixed_iterator rhs) noexcept { + return lhs.m_data == rhs.m_data; + } + + friend bool operator!=(const_fixed_iterator lhs, const_fixed_iterator rhs) noexcept { + return !(lhs == rhs); + } + + friend bool operator<(const_fixed_iterator lhs, const_fixed_iterator rhs) noexcept { + return lhs.m_data < rhs.m_data; + } + + friend bool operator>(const_fixed_iterator lhs, const_fixed_iterator rhs) noexcept { + return rhs < lhs; + } + + friend bool operator<=(const_fixed_iterator lhs, const_fixed_iterator rhs) noexcept { + return !(lhs > rhs); + } + + friend bool operator>=(const_fixed_iterator lhs, const_fixed_iterator rhs) noexcept { + return !(lhs < rhs); + } + + const_fixed_iterator& operator+=(difference_type val) noexcept { + m_data += (sizeof(value_type) * val); + return *this; + } + + friend const_fixed_iterator operator+(const_fixed_iterator lhs, difference_type rhs) noexcept { + const_fixed_iterator tmp{lhs}; + tmp.m_data += (sizeof(value_type) * rhs); + return tmp; + } + + friend const_fixed_iterator operator+(difference_type lhs, const_fixed_iterator rhs) noexcept { + const_fixed_iterator tmp{rhs}; + tmp.m_data += (sizeof(value_type) * lhs); + return tmp; + } + + const_fixed_iterator& operator-=(difference_type val) noexcept { + m_data -= (sizeof(value_type) * val); + return *this; + } + + friend const_fixed_iterator operator-(const_fixed_iterator lhs, difference_type rhs) noexcept { + const_fixed_iterator tmp{lhs}; + tmp.m_data -= (sizeof(value_type) * rhs); + return tmp; + } + + friend difference_type operator-(const_fixed_iterator lhs, const_fixed_iterator rhs) noexcept { + return static_cast(lhs.m_data - rhs.m_data) / static_cast(sizeof(T)); + } + + value_type operator[](difference_type n) const noexcept { + return *(*this + n); + } + + /// @endcond + +}; // class const_fixed_iterator + +/** + * A forward iterator used for accessing packed repeated varint fields + * (int32, uint32, int64, uint64, bool, enum). + */ +template +class const_varint_iterator { + +protected: + + /// Pointer to current iterator position + const char* m_data = nullptr; // NOLINT(misc-non-private-member-variables-in-classes, cppcoreguidelines-non-private-member-variables-in-classes,-warnings-as-errors) + + /// Pointer to end iterator position + const char* m_end = nullptr; // NOLINT(misc-non-private-member-variables-in-classes, cppcoreguidelines-non-private-member-variables-in-classes,-warnings-as-errors) + +public: + + /// @cond usual iterator functions not documented + + using iterator_category = std::forward_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + + static difference_type distance(const_varint_iterator begin, const_varint_iterator end) noexcept { + // The "distance" between default initialized const_varint_iterator's + // is always 0. + if (!begin.m_data) { + return 0; + } + // We know that each varint contains exactly one byte with the most + // significant bit not set. We can use this to quickly figure out + // how many varints there are without actually decoding the varints. + return std::count_if(begin.m_data, end.m_data, [](char c) noexcept { + return (static_cast(c) & 0x80U) == 0; + }); + } + + const_varint_iterator() noexcept = default; + + const_varint_iterator(const char* data, const char* end) noexcept : + m_data{data}, + m_end{end} { + } + + const_varint_iterator(const const_varint_iterator&) noexcept = default; + const_varint_iterator(const_varint_iterator&&) noexcept = default; + + const_varint_iterator& operator=(const const_varint_iterator&) noexcept = default; + const_varint_iterator& operator=(const_varint_iterator&&) noexcept = default; + + ~const_varint_iterator() noexcept = default; + + value_type operator*() const { + protozero_assert(m_data); + const char* d = m_data; // will be thrown away + return static_cast(decode_varint(&d, m_end)); + } + + const_varint_iterator& operator++() { + protozero_assert(m_data); + skip_varint(&m_data, m_end); + return *this; + } + + const_varint_iterator operator++(int) { + protozero_assert(m_data); + const const_varint_iterator tmp{*this}; + ++(*this); + return tmp; + } + + bool operator==(const const_varint_iterator& rhs) const noexcept { + return m_data == rhs.m_data && m_end == rhs.m_end; + } + + bool operator!=(const const_varint_iterator& rhs) const noexcept { + return !(*this == rhs); + } + + /// @endcond + +}; // class const_varint_iterator + +/** + * A forward iterator used for accessing packed repeated svarint fields + * (sint32, sint64). + */ +template +class const_svarint_iterator : public const_varint_iterator { + +public: + + /// @cond usual iterator functions not documented + + using iterator_category = std::forward_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + + const_svarint_iterator() noexcept : + const_varint_iterator{} { + } + + const_svarint_iterator(const char* data, const char* end) noexcept : + const_varint_iterator{data, end} { + } + + const_svarint_iterator(const const_svarint_iterator&) = default; + const_svarint_iterator(const_svarint_iterator&&) noexcept = default; + + const_svarint_iterator& operator=(const const_svarint_iterator&) = default; + const_svarint_iterator& operator=(const_svarint_iterator&&) noexcept = default; + + ~const_svarint_iterator() = default; + + value_type operator*() const { + protozero_assert(this->m_data); + const char* d = this->m_data; // will be thrown away + return static_cast(decode_zigzag64(decode_varint(&d, this->m_end))); + } + + const_svarint_iterator& operator++() { + protozero_assert(this->m_data); + skip_varint(&this->m_data, this->m_end); + return *this; + } + + const_svarint_iterator operator++(int) { + protozero_assert(this->m_data); + const const_svarint_iterator tmp{*this}; + ++(*this); + return tmp; + } + + /// @endcond + +}; // class const_svarint_iterator + +} // end namespace protozero + +namespace std { + + // Specialize std::distance for all the protozero iterators. Because + // functions can't be partially specialized, we have to do this for + // every value_type we are using. + + /// @cond individual overloads do not need to be documented + + template <> + inline typename protozero::const_varint_iterator::difference_type + distance>(protozero::const_varint_iterator first, // NOLINT(readability-inconsistent-declaration-parameter-name) + protozero::const_varint_iterator last) { + return protozero::const_varint_iterator::distance(first, last); + } + + template <> + inline typename protozero::const_varint_iterator::difference_type + distance>(protozero::const_varint_iterator first, // NOLINT(readability-inconsistent-declaration-parameter-name) + protozero::const_varint_iterator last) { + return protozero::const_varint_iterator::distance(first, last); + } + + template <> + inline typename protozero::const_varint_iterator::difference_type + distance>(protozero::const_varint_iterator first, // NOLINT(readability-inconsistent-declaration-parameter-name) + protozero::const_varint_iterator last) { + return protozero::const_varint_iterator::distance(first, last); + } + + template <> + inline typename protozero::const_varint_iterator::difference_type + distance>(protozero::const_varint_iterator first, // NOLINT(readability-inconsistent-declaration-parameter-name) + protozero::const_varint_iterator last) { + return protozero::const_varint_iterator::distance(first, last); + } + + template <> + inline typename protozero::const_svarint_iterator::difference_type + distance>(protozero::const_svarint_iterator first, // NOLINT(readability-inconsistent-declaration-parameter-name) + protozero::const_svarint_iterator last) { + return protozero::const_svarint_iterator::distance(first, last); + } + + template <> + inline typename protozero::const_svarint_iterator::difference_type + distance>(protozero::const_svarint_iterator first, // NOLINT(readability-inconsistent-declaration-parameter-name) + protozero::const_svarint_iterator last) { + return protozero::const_svarint_iterator::distance(first, last); + } + + /// @endcond + +} // end namespace std + +#endif // PROTOZERO_ITERATORS_HPP diff --git a/include/protozero/pbf_builder.hpp b/include/protozero/pbf_builder.hpp new file mode 100644 index 00000000..71a2dec2 --- /dev/null +++ b/include/protozero/pbf_builder.hpp @@ -0,0 +1,32 @@ +#ifndef PROTOZERO_PBF_BUILDER_HPP +#define PROTOZERO_PBF_BUILDER_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file pbf_builder.hpp + * + * @brief Contains the pbf_builder template class. + */ + +#include "basic_pbf_builder.hpp" +#include "pbf_writer.hpp" + +#include + +namespace protozero { + +/// Specialization of basic_pbf_builder using std::string as buffer type. +template +using pbf_builder = basic_pbf_builder; + +} // end namespace protozero + +#endif // PROTOZERO_PBF_BUILDER_HPP diff --git a/include/protozero/pbf_message.hpp b/include/protozero/pbf_message.hpp new file mode 100644 index 00000000..d7fd8b5d --- /dev/null +++ b/include/protozero/pbf_message.hpp @@ -0,0 +1,184 @@ +#ifndef PROTOZERO_PBF_MESSAGE_HPP +#define PROTOZERO_PBF_MESSAGE_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file pbf_message.hpp + * + * @brief Contains the pbf_message template class. + */ + +#include "pbf_reader.hpp" +#include "types.hpp" + +#include + +namespace protozero { + +/** + * This class represents a protobuf message. Either a top-level message or + * a nested sub-message. Top-level messages can be created from any buffer + * with a pointer and length: + * + * @code + * enum class Message : protozero::pbf_tag_type { + * ... + * }; + * + * std::string buffer; + * // fill buffer... + * pbf_message message{buffer.data(), buffer.size()}; + * @endcode + * + * Sub-messages are created using get_message(): + * + * @code + * enum class SubMessage : protozero::pbf_tag_type { + * ... + * }; + * + * pbf_message message{...}; + * message.next(); + * pbf_message submessage = message.get_message(); + * @endcode + * + * All methods of the pbf_message class except get_bytes() and get_string() + * provide the strong exception guarantee, ie they either succeed or do not + * change the pbf_message object they are called on. Use the get_data() method + * instead of get_bytes() or get_string(), if you need this guarantee. + * + * This template class is based on the pbf_reader class and has all the same + * methods. The difference is that whereever the pbf_reader class takes an + * integer tag, this template class takes a tag of the template type T. + * + * Read the tutorial to understand how this class is used. + */ +template +class pbf_message : public pbf_reader { + + static_assert(std::is_same::type>::value, + "T must be enum with underlying type protozero::pbf_tag_type"); + +public: + + /// The type of messages this class will read. + using enum_type = T; + + /** + * Construct a pbf_message. All arguments are forwarded to the pbf_reader + * parent class. + */ + template + pbf_message(Args&&... args) noexcept : // NOLINT(google-explicit-constructor, hicpp-explicit-conversions) + pbf_reader{std::forward(args)...} { + } + + /** + * Set next field in the message as the current field. This is usually + * called in a while loop: + * + * @code + * pbf_message<...> message(...); + * while (message.next()) { + * // handle field + * } + * @endcode + * + * @returns `true` if there is a next field, `false` if not. + * @pre There must be no current field. + * @post If it returns `true` there is a current field now. + */ + bool next() { + return pbf_reader::next(); + } + + /** + * Set next field with given tag in the message as the current field. + * Fields with other tags are skipped. This is usually called in a while + * loop for repeated fields: + * + * @code + * pbf_message message{...}; + * while (message.next(Example1::repeated_fixed64_r)) { + * // handle field + * } + * @endcode + * + * or you can call it just once to get the one field with this tag: + * + * @code + * pbf_message message{...}; + * if (message.next(Example1::required_uint32_x)) { + * // handle field + * } + * @endcode + * + * Note that this will not check the wire type. The two-argument version + * of this function will also check the wire type. + * + * @returns `true` if there is a next field with this tag. + * @pre There must be no current field. + * @post If it returns `true` there is a current field now with the given tag. + */ + bool next(T next_tag) { + return pbf_reader::next(pbf_tag_type(next_tag)); + } + + /** + * Set next field with given tag and wire type in the message as the + * current field. Fields with other tags are skipped. This is usually + * called in a while loop for repeated fields: + * + * @code + * pbf_message message{...}; + * while (message.next(Example1::repeated_fixed64_r, pbf_wire_type::varint)) { + * // handle field + * } + * @endcode + * + * or you can call it just once to get the one field with this tag: + * + * @code + * pbf_message message{...}; + * if (message.next(Example1::required_uint32_x, pbf_wire_type::varint)) { + * // handle field + * } + * @endcode + * + * Note that this will also check the wire type. The one-argument version + * of this function will not check the wire type. + * + * @returns `true` if there is a next field with this tag. + * @pre There must be no current field. + * @post If it returns `true` there is a current field now with the given tag. + */ + bool next(T next_tag, pbf_wire_type type) { + return pbf_reader::next(pbf_tag_type(next_tag), type); + } + + /** + * The tag of the current field. The tag is the enum value for the field + * number from the description in the .proto file. + * + * Call next() before calling this function to set the current field. + * + * @returns tag of the current field. + * @pre There must be a current field (ie. next() must have returned `true`). + */ + T tag() const noexcept { + return T(pbf_reader::tag()); + } + +}; // class pbf_message + +} // end namespace protozero + +#endif // PROTOZERO_PBF_MESSAGE_HPP diff --git a/include/protozero/pbf_reader.hpp b/include/protozero/pbf_reader.hpp new file mode 100644 index 00000000..92bfdee5 --- /dev/null +++ b/include/protozero/pbf_reader.hpp @@ -0,0 +1,977 @@ +#ifndef PROTOZERO_PBF_READER_HPP +#define PROTOZERO_PBF_READER_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file pbf_reader.hpp + * + * @brief Contains the pbf_reader class. + */ + +#include "config.hpp" +#include "data_view.hpp" +#include "exception.hpp" +#include "iterators.hpp" +#include "types.hpp" +#include "varint.hpp" + +#if PROTOZERO_BYTE_ORDER != PROTOZERO_LITTLE_ENDIAN +# include +#endif + +#include +#include +#include +#include +#include + +namespace protozero { + +/** + * This class represents a protobuf message. Either a top-level message or + * a nested sub-message. Top-level messages can be created from any buffer + * with a pointer and length: + * + * @code + * std::string buffer; + * // fill buffer... + * pbf_reader message{buffer.data(), buffer.size()}; + * @endcode + * + * Sub-messages are created using get_message(): + * + * @code + * pbf_reader message{...}; + * message.next(); + * pbf_reader submessage = message.get_message(); + * @endcode + * + * All methods of the pbf_reader class except get_bytes() and get_string() + * provide the strong exception guarantee, ie they either succeed or do not + * change the pbf_reader object they are called on. Use the get_view() method + * instead of get_bytes() or get_string(), if you need this guarantee. + */ +class pbf_reader { + + // A pointer to the next unread data. + const char* m_data = nullptr; + + // A pointer to one past the end of data. + const char* m_end = nullptr; + + // The wire type of the current field. + pbf_wire_type m_wire_type = pbf_wire_type::unknown; + + // The tag of the current field. + pbf_tag_type m_tag = 0; + + template + T get_fixed() { + T result; + const char* data = m_data; + skip_bytes(sizeof(T)); + std::memcpy(&result, data, sizeof(T)); +#if PROTOZERO_BYTE_ORDER != PROTOZERO_LITTLE_ENDIAN + byteswap_inplace(&result); +#endif + return result; + } + + template + iterator_range> packed_fixed() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + const auto len = get_len_and_skip(); + if (len % sizeof(T) != 0) { + throw invalid_length_exception{}; + } + return {const_fixed_iterator(m_data - len), + const_fixed_iterator(m_data)}; + } + + template + T get_varint() { + const auto val = static_cast(decode_varint(&m_data, m_end)); + return val; + } + + template + T get_svarint() { + protozero_assert((has_wire_type(pbf_wire_type::varint) || has_wire_type(pbf_wire_type::length_delimited)) && "not a varint"); + return static_cast(decode_zigzag64(decode_varint(&m_data, m_end))); + } + + pbf_length_type get_length() { + return get_varint(); + } + + void skip_bytes(pbf_length_type len) { + if (m_end - m_data < static_cast(len)) { + throw end_of_buffer_exception{}; + } + m_data += len; + +#ifndef NDEBUG + // In debug builds reset the tag to zero so that we can detect (some) + // wrong code. + m_tag = 0; +#endif + } + + pbf_length_type get_len_and_skip() { + const auto len = get_length(); + skip_bytes(len); + return len; + } + + template + iterator_range get_packed() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + const auto len = get_len_and_skip(); + return {T{m_data - len, m_data}, + T{m_data, m_data}}; + } + +public: + + /** + * Construct a pbf_reader message from a data_view. The pointer from the + * data_view will be stored inside the pbf_reader object, no data is + * copied. So you must make sure the view stays valid as long as the + * pbf_reader object is used. + * + * The buffer must contain a complete protobuf message. + * + * @post There is no current field. + */ + explicit pbf_reader(const data_view& view) noexcept + : m_data{view.data()}, + m_end{view.data() + view.size()} { + } + + /** + * Construct a pbf_reader message from a data pointer and a length. The + * pointer will be stored inside the pbf_reader object, no data is copied. + * So you must make sure the buffer stays valid as long as the pbf_reader + * object is used. + * + * The buffer must contain a complete protobuf message. + * + * @post There is no current field. + */ + pbf_reader(const char* data, std::size_t size) noexcept + : m_data{data}, + m_end{data + size} { + } + +#ifndef PROTOZERO_STRICT_API + /** + * Construct a pbf_reader message from a data pointer and a length. The + * pointer will be stored inside the pbf_reader object, no data is copied. + * So you must make sure the buffer stays valid as long as the pbf_reader + * object is used. + * + * The buffer must contain a complete protobuf message. + * + * @post There is no current field. + * @deprecated Use one of the other constructors. + */ + explicit pbf_reader(const std::pair& data) noexcept + : m_data{data.first}, + m_end{data.first + data.second} { + } +#endif + + /** + * Construct a pbf_reader message from a std::string. A pointer to the + * string internals will be stored inside the pbf_reader object, no data + * is copied. So you must make sure the string is unchanged as long as the + * pbf_reader object is used. + * + * The string must contain a complete protobuf message. + * + * @post There is no current field. + */ + explicit pbf_reader(const std::string& data) noexcept + : m_data{data.data()}, + m_end{data.data() + data.size()} { + } + + /** + * pbf_reader can be default constructed and behaves like it has an empty + * buffer. + */ + pbf_reader() noexcept = default; + + /// pbf_reader messages can be copied trivially. + pbf_reader(const pbf_reader&) noexcept = default; + + /// pbf_reader messages can be moved trivially. + pbf_reader(pbf_reader&&) noexcept = default; + + /// pbf_reader messages can be copied trivially. + pbf_reader& operator=(const pbf_reader& other) noexcept = default; + + /// pbf_reader messages can be moved trivially. + pbf_reader& operator=(pbf_reader&& other) noexcept = default; + + ~pbf_reader() = default; + + /** + * Swap the contents of this object with the other. + * + * @param other Other object to swap data with. + */ + void swap(pbf_reader& other) noexcept { + using std::swap; + swap(m_data, other.m_data); + swap(m_end, other.m_end); + swap(m_wire_type, other.m_wire_type); + swap(m_tag, other.m_tag); + } + + /** + * In a boolean context the pbf_reader class evaluates to `true` if there + * are still fields available and to `false` if the last field has been + * read. + */ + operator bool() const noexcept { // NOLINT(google-explicit-constructor, hicpp-explicit-conversions) + return m_data != m_end; + } + + /** + * Get a view of the not yet read data. + */ + data_view data() const noexcept { + return {m_data, static_cast(m_end - m_data)}; + } + + /** + * Return the length in bytes of the current message. If you have + * already called next() and/or any of the get_*() functions, this will + * return the remaining length. + * + * This can, for instance, be used to estimate the space needed for a + * buffer. Of course you have to know reasonably well what data to expect + * and how it is encoded for this number to have any meaning. + */ + std::size_t length() const noexcept { + return std::size_t(m_end - m_data); + } + + /** + * Set next field in the message as the current field. This is usually + * called in a while loop: + * + * @code + * pbf_reader message(...); + * while (message.next()) { + * // handle field + * } + * @endcode + * + * @returns `true` if there is a next field, `false` if not. + * @pre There must be no current field. + * @post If it returns `true` there is a current field now. + */ + bool next() { + if (m_data == m_end) { + return false; + } + + const auto value = get_varint(); + m_tag = pbf_tag_type(value >> 3U); + + // tags 0 and 19000 to 19999 are not allowed as per + // https://developers.google.com/protocol-buffers/docs/proto#assigning-tags + if (m_tag == 0 || (m_tag >= 19000 && m_tag <= 19999)) { + throw invalid_tag_exception{}; + } + + m_wire_type = pbf_wire_type(value & 0x07U); + switch (m_wire_type) { + case pbf_wire_type::varint: + case pbf_wire_type::fixed64: + case pbf_wire_type::length_delimited: + case pbf_wire_type::fixed32: + break; + default: + throw unknown_pbf_wire_type_exception{}; + } + + return true; + } + + /** + * Set next field with given tag in the message as the current field. + * Fields with other tags are skipped. This is usually called in a while + * loop for repeated fields: + * + * @code + * pbf_reader message{...}; + * while (message.next(17)) { + * // handle field + * } + * @endcode + * + * or you can call it just once to get the one field with this tag: + * + * @code + * pbf_reader message{...}; + * if (message.next(17)) { + * // handle field + * } + * @endcode + * + * Note that this will not check the wire type. The two-argument version + * of this function will also check the wire type. + * + * @returns `true` if there is a next field with this tag. + * @pre There must be no current field. + * @post If it returns `true` there is a current field now with the given tag. + */ + bool next(pbf_tag_type next_tag) { + while (next()) { + if (m_tag == next_tag) { + return true; + } + skip(); + } + return false; + } + + /** + * Set next field with given tag and wire type in the message as the + * current field. Fields with other tags are skipped. This is usually + * called in a while loop for repeated fields: + * + * @code + * pbf_reader message{...}; + * while (message.next(17, pbf_wire_type::varint)) { + * // handle field + * } + * @endcode + * + * or you can call it just once to get the one field with this tag: + * + * @code + * pbf_reader message{...}; + * if (message.next(17, pbf_wire_type::varint)) { + * // handle field + * } + * @endcode + * + * Note that this will also check the wire type. The one-argument version + * of this function will not check the wire type. + * + * @returns `true` if there is a next field with this tag. + * @pre There must be no current field. + * @post If it returns `true` there is a current field now with the given tag. + */ + bool next(pbf_tag_type next_tag, pbf_wire_type type) { + while (next()) { + if (m_tag == next_tag && m_wire_type == type) { + return true; + } + skip(); + } + return false; + } + + /** + * The tag of the current field. The tag is the field number from the + * description in the .proto file. + * + * Call next() before calling this function to set the current field. + * + * @returns tag of the current field. + * @pre There must be a current field (ie. next() must have returned `true`). + */ + pbf_tag_type tag() const noexcept { + return m_tag; + } + + /** + * Get the wire type of the current field. The wire types are: + * + * * 0 - varint + * * 1 - 64 bit + * * 2 - length-delimited + * * 5 - 32 bit + * + * All other types are illegal. + * + * Call next() before calling this function to set the current field. + * + * @returns wire type of the current field. + * @pre There must be a current field (ie. next() must have returned `true`). + */ + pbf_wire_type wire_type() const noexcept { + return m_wire_type; + } + + /** + * Get the tag and wire type of the current field in one integer suitable + * for comparison with a switch statement. + * + * Use it like this: + * + * @code + * pbf_reader message{...}; + * while (message.next()) { + * switch (message.tag_and_type()) { + * case tag_and_type(17, pbf_wire_type::length_delimited): + * .... + * break; + * case tag_and_type(21, pbf_wire_type::varint): + * .... + * break; + * default: + * message.skip(); + * } + * } + * @endcode + */ + uint32_t tag_and_type() const noexcept { + return protozero::tag_and_type(tag(), wire_type()); + } + + /** + * Check the wire type of the current field. + * + * @returns `true` if the current field has the given wire type. + * @pre There must be a current field (ie. next() must have returned `true`). + */ + bool has_wire_type(pbf_wire_type type) const noexcept { + return wire_type() == type; + } + + /** + * Consume the current field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @post The current field was consumed and there is no current field now. + */ + void skip() { + protozero_assert(tag() != 0 && "call next() before calling skip()"); + switch (wire_type()) { + case pbf_wire_type::varint: + skip_varint(&m_data, m_end); + break; + case pbf_wire_type::fixed64: + skip_bytes(8); + break; + case pbf_wire_type::length_delimited: + skip_bytes(get_length()); + break; + case pbf_wire_type::fixed32: + skip_bytes(4); + break; + default: + break; + } + } + + ///@{ + /** + * @name Scalar field accessor functions + */ + + /** + * Consume and return value of current "bool" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "bool". + * @post The current field was consumed and there is no current field now. + */ + bool get_bool() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + protozero_assert(has_wire_type(pbf_wire_type::varint) && "not a varint"); + const bool result = m_data[0] != 0; + skip_varint(&m_data, m_end); + return result; + } + + /** + * Consume and return value of current "enum" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "enum". + * @post The current field was consumed and there is no current field now. + */ + int32_t get_enum() { + protozero_assert(has_wire_type(pbf_wire_type::varint) && "not a varint"); + return get_varint(); + } + + /** + * Consume and return value of current "int32" varint field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "int32". + * @post The current field was consumed and there is no current field now. + */ + int32_t get_int32() { + protozero_assert(has_wire_type(pbf_wire_type::varint) && "not a varint"); + return get_varint(); + } + + /** + * Consume and return value of current "sint32" varint field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "sint32". + * @post The current field was consumed and there is no current field now. + */ + int32_t get_sint32() { + protozero_assert(has_wire_type(pbf_wire_type::varint) && "not a varint"); + return get_svarint(); + } + + /** + * Consume and return value of current "uint32" varint field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "uint32". + * @post The current field was consumed and there is no current field now. + */ + uint32_t get_uint32() { + protozero_assert(has_wire_type(pbf_wire_type::varint) && "not a varint"); + return get_varint(); + } + + /** + * Consume and return value of current "int64" varint field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "int64". + * @post The current field was consumed and there is no current field now. + */ + int64_t get_int64() { + protozero_assert(has_wire_type(pbf_wire_type::varint) && "not a varint"); + return get_varint(); + } + + /** + * Consume and return value of current "sint64" varint field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "sint64". + * @post The current field was consumed and there is no current field now. + */ + int64_t get_sint64() { + protozero_assert(has_wire_type(pbf_wire_type::varint) && "not a varint"); + return get_svarint(); + } + + /** + * Consume and return value of current "uint64" varint field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "uint64". + * @post The current field was consumed and there is no current field now. + */ + uint64_t get_uint64() { + protozero_assert(has_wire_type(pbf_wire_type::varint) && "not a varint"); + return get_varint(); + } + + /** + * Consume and return value of current "fixed32" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "fixed32". + * @post The current field was consumed and there is no current field now. + */ + uint32_t get_fixed32() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + protozero_assert(has_wire_type(pbf_wire_type::fixed32) && "not a 32-bit fixed"); + return get_fixed(); + } + + /** + * Consume and return value of current "sfixed32" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "sfixed32". + * @post The current field was consumed and there is no current field now. + */ + int32_t get_sfixed32() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + protozero_assert(has_wire_type(pbf_wire_type::fixed32) && "not a 32-bit fixed"); + return get_fixed(); + } + + /** + * Consume and return value of current "fixed64" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "fixed64". + * @post The current field was consumed and there is no current field now. + */ + uint64_t get_fixed64() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + protozero_assert(has_wire_type(pbf_wire_type::fixed64) && "not a 64-bit fixed"); + return get_fixed(); + } + + /** + * Consume and return value of current "sfixed64" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "sfixed64". + * @post The current field was consumed and there is no current field now. + */ + int64_t get_sfixed64() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + protozero_assert(has_wire_type(pbf_wire_type::fixed64) && "not a 64-bit fixed"); + return get_fixed(); + } + + /** + * Consume and return value of current "float" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "float". + * @post The current field was consumed and there is no current field now. + */ + float get_float() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + protozero_assert(has_wire_type(pbf_wire_type::fixed32) && "not a 32-bit fixed"); + return get_fixed(); + } + + /** + * Consume and return value of current "double" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "double". + * @post The current field was consumed and there is no current field now. + */ + double get_double() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + protozero_assert(has_wire_type(pbf_wire_type::fixed64) && "not a 64-bit fixed"); + return get_fixed(); + } + + /** + * Consume and return value of current "bytes", "string", or "message" + * field. + * + * @returns A data_view object. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "bytes", "string", or "message". + * @post The current field was consumed and there is no current field now. + */ + data_view get_view() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + protozero_assert(has_wire_type(pbf_wire_type::length_delimited) && "not of type string, bytes or message"); + const auto len = get_len_and_skip(); + return {m_data - len, len}; + } + +#ifndef PROTOZERO_STRICT_API + /** + * Consume and return value of current "bytes" or "string" field. + * + * @returns A pair with a pointer to the data and the length of the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "bytes" or "string". + * @post The current field was consumed and there is no current field now. + */ + std::pair get_data() { + protozero_assert(tag() != 0 && "call next() before accessing field value"); + protozero_assert(has_wire_type(pbf_wire_type::length_delimited) && "not of type string, bytes or message"); + const auto len = get_len_and_skip(); + return {m_data - len, len}; + } +#endif + + /** + * Consume and return value of current "bytes" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "bytes". + * @post The current field was consumed and there is no current field now. + */ + std::string get_bytes() { + return std::string(get_view()); + } + + /** + * Consume and return value of current "string" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "string". + * @post The current field was consumed and there is no current field now. + */ + std::string get_string() { + return std::string(get_view()); + } + + /** + * Consume and return value of current "message" field. + * + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "message". + * @post The current field was consumed and there is no current field now. + */ + pbf_reader get_message() { + return pbf_reader{get_view()}; + } + + ///@} + + /// Forward iterator for iterating over bool (int32 varint) values. + using const_bool_iterator = const_varint_iterator< int32_t>; + + /// Forward iterator for iterating over enum (int32 varint) values. + using const_enum_iterator = const_varint_iterator< int32_t>; + + /// Forward iterator for iterating over int32 (varint) values. + using const_int32_iterator = const_varint_iterator< int32_t>; + + /// Forward iterator for iterating over sint32 (varint) values. + using const_sint32_iterator = const_svarint_iterator; + + /// Forward iterator for iterating over uint32 (varint) values. + using const_uint32_iterator = const_varint_iterator; + + /// Forward iterator for iterating over int64 (varint) values. + using const_int64_iterator = const_varint_iterator< int64_t>; + + /// Forward iterator for iterating over sint64 (varint) values. + using const_sint64_iterator = const_svarint_iterator; + + /// Forward iterator for iterating over uint64 (varint) values. + using const_uint64_iterator = const_varint_iterator; + + /// Forward iterator for iterating over fixed32 values. + using const_fixed32_iterator = const_fixed_iterator; + + /// Forward iterator for iterating over sfixed32 values. + using const_sfixed32_iterator = const_fixed_iterator; + + /// Forward iterator for iterating over fixed64 values. + using const_fixed64_iterator = const_fixed_iterator; + + /// Forward iterator for iterating over sfixed64 values. + using const_sfixed64_iterator = const_fixed_iterator; + + /// Forward iterator for iterating over float values. + using const_float_iterator = const_fixed_iterator; + + /// Forward iterator for iterating over double values. + using const_double_iterator = const_fixed_iterator; + + ///@{ + /** + * @name Repeated packed field accessor functions + */ + + /** + * Consume current "repeated packed bool" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed bool". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_bool() { + return get_packed(); + } + + /** + * Consume current "repeated packed enum" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed enum". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_enum() { + return get_packed(); + } + + /** + * Consume current "repeated packed int32" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed int32". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_int32() { + return get_packed(); + } + + /** + * Consume current "repeated packed sint32" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed sint32". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_sint32() { + return get_packed(); + } + + /** + * Consume current "repeated packed uint32" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed uint32". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_uint32() { + return get_packed(); + } + + /** + * Consume current "repeated packed int64" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed int64". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_int64() { + return get_packed(); + } + + /** + * Consume current "repeated packed sint64" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed sint64". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_sint64() { + return get_packed(); + } + + /** + * Consume current "repeated packed uint64" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed uint64". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_uint64() { + return get_packed(); + } + + /** + * Consume current "repeated packed fixed32" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed fixed32". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_fixed32() { + return packed_fixed(); + } + + /** + * Consume current "repeated packed sfixed32" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed sfixed32". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_sfixed32() { + return packed_fixed(); + } + + /** + * Consume current "repeated packed fixed64" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed fixed64". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_fixed64() { + return packed_fixed(); + } + + /** + * Consume current "repeated packed sfixed64" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed sfixed64". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_sfixed64() { + return packed_fixed(); + } + + /** + * Consume current "repeated packed float" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed float". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_float() { + return packed_fixed(); + } + + /** + * Consume current "repeated packed double" field. + * + * @returns a pair of iterators to the beginning and one past the end of + * the data. + * @pre There must be a current field (ie. next() must have returned `true`). + * @pre The current field must be of type "repeated packed double". + * @post The current field was consumed and there is no current field now. + */ + iterator_range get_packed_double() { + return packed_fixed(); + } + + ///@} + +}; // class pbf_reader + +/** + * Swap two pbf_reader objects. + * + * @param lhs First object. + * @param rhs Second object. + */ +inline void swap(pbf_reader& lhs, pbf_reader& rhs) noexcept { + lhs.swap(rhs); +} + +} // end namespace protozero + +#endif // PROTOZERO_PBF_READER_HPP diff --git a/include/protozero/pbf_writer.hpp b/include/protozero/pbf_writer.hpp new file mode 100644 index 00000000..9a07bd5b --- /dev/null +++ b/include/protozero/pbf_writer.hpp @@ -0,0 +1,76 @@ +#ifndef PROTOZERO_PBF_WRITER_HPP +#define PROTOZERO_PBF_WRITER_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file pbf_writer.hpp + * + * @brief Contains the pbf_writer class. + */ + +#include "basic_pbf_writer.hpp" +#include "buffer_string.hpp" + +#include +#include + +namespace protozero { + +/** + * Specialization of basic_pbf_writer using std::string as buffer type. + */ +using pbf_writer = basic_pbf_writer; + +/// Class for generating packed repeated bool fields. +using packed_field_bool = detail::packed_field_varint; + +/// Class for generating packed repeated enum fields. +using packed_field_enum = detail::packed_field_varint; + +/// Class for generating packed repeated int32 fields. +using packed_field_int32 = detail::packed_field_varint; + +/// Class for generating packed repeated sint32 fields. +using packed_field_sint32 = detail::packed_field_svarint; + +/// Class for generating packed repeated uint32 fields. +using packed_field_uint32 = detail::packed_field_varint; + +/// Class for generating packed repeated int64 fields. +using packed_field_int64 = detail::packed_field_varint; + +/// Class for generating packed repeated sint64 fields. +using packed_field_sint64 = detail::packed_field_svarint; + +/// Class for generating packed repeated uint64 fields. +using packed_field_uint64 = detail::packed_field_varint; + +/// Class for generating packed repeated fixed32 fields. +using packed_field_fixed32 = detail::packed_field_fixed; + +/// Class for generating packed repeated sfixed32 fields. +using packed_field_sfixed32 = detail::packed_field_fixed; + +/// Class for generating packed repeated fixed64 fields. +using packed_field_fixed64 = detail::packed_field_fixed; + +/// Class for generating packed repeated sfixed64 fields. +using packed_field_sfixed64 = detail::packed_field_fixed; + +/// Class for generating packed repeated float fields. +using packed_field_float = detail::packed_field_fixed; + +/// Class for generating packed repeated double fields. +using packed_field_double = detail::packed_field_fixed; + +} // end namespace protozero + +#endif // PROTOZERO_PBF_WRITER_HPP diff --git a/include/protozero/types.hpp b/include/protozero/types.hpp new file mode 100644 index 00000000..3aefddfb --- /dev/null +++ b/include/protozero/types.hpp @@ -0,0 +1,66 @@ +#ifndef PROTOZERO_TYPES_HPP +#define PROTOZERO_TYPES_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file types.hpp + * + * @brief Contains the declaration of low-level types used in the pbf format. + */ + +#include "config.hpp" + +#include +#include +#include +#include +#include +#include + +namespace protozero { + +/** + * The type used for field tags (field numbers). + */ +using pbf_tag_type = uint32_t; + +/** + * The type used to encode type information. + * See the table on + * https://developers.google.com/protocol-buffers/docs/encoding + */ +enum class pbf_wire_type : uint32_t { + varint = 0, // int32/64, uint32/64, sint32/64, bool, enum + fixed64 = 1, // fixed64, sfixed64, double + length_delimited = 2, // string, bytes, nested messages, packed repeated fields + fixed32 = 5, // fixed32, sfixed32, float + unknown = 99 // used for default setting in this library +}; + +/** + * Get the tag and wire type of the current field in one integer suitable + * for comparison with a switch statement. + * + * See pbf_reader.tag_and_type() for an example how to use this. + */ +template +constexpr inline uint32_t tag_and_type(T tag, pbf_wire_type wire_type) noexcept { + return (static_cast(static_cast(tag)) << 3U) | static_cast(wire_type); +} + +/** + * The type used for length values, such as the length of a field. + */ +using pbf_length_type = uint32_t; + +} // end namespace protozero + +#endif // PROTOZERO_TYPES_HPP diff --git a/include/protozero/varint.hpp b/include/protozero/varint.hpp new file mode 100644 index 00000000..b4648a44 --- /dev/null +++ b/include/protozero/varint.hpp @@ -0,0 +1,245 @@ +#ifndef PROTOZERO_VARINT_HPP +#define PROTOZERO_VARINT_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file varint.hpp + * + * @brief Contains low-level varint and zigzag encoding and decoding functions. + */ + +#include "buffer_tmpl.hpp" +#include "exception.hpp" + +#include + +namespace protozero { + +/** + * The maximum length of a 64 bit varint. + */ +constexpr const int8_t max_varint_length = sizeof(uint64_t) * 8 / 7 + 1; + +namespace detail { + + // from https://github.com/facebook/folly/blob/master/folly/Varint.h + inline uint64_t decode_varint_impl(const char** data, const char* end) { + const auto* begin = reinterpret_cast(*data); + const auto* iend = reinterpret_cast(end); + const int8_t* p = begin; + uint64_t val = 0; + + if (iend - begin >= max_varint_length) { // fast path + do { + int64_t b = *p++; + val = ((uint64_t(b) & 0x7fU) ); if (b >= 0) { break; } + b = *p++; val |= ((uint64_t(b) & 0x7fU) << 7U); if (b >= 0) { break; } + b = *p++; val |= ((uint64_t(b) & 0x7fU) << 14U); if (b >= 0) { break; } + b = *p++; val |= ((uint64_t(b) & 0x7fU) << 21U); if (b >= 0) { break; } + b = *p++; val |= ((uint64_t(b) & 0x7fU) << 28U); if (b >= 0) { break; } + b = *p++; val |= ((uint64_t(b) & 0x7fU) << 35U); if (b >= 0) { break; } + b = *p++; val |= ((uint64_t(b) & 0x7fU) << 42U); if (b >= 0) { break; } + b = *p++; val |= ((uint64_t(b) & 0x7fU) << 49U); if (b >= 0) { break; } + b = *p++; val |= ((uint64_t(b) & 0x7fU) << 56U); if (b >= 0) { break; } + b = *p++; val |= ((uint64_t(b) & 0x01U) << 63U); if (b >= 0) { break; } + throw varint_too_long_exception{}; + } while (false); + } else { + unsigned int shift = 0; + while (p != iend && *p < 0) { + val |= (uint64_t(*p++) & 0x7fU) << shift; + shift += 7; + } + if (p == iend) { + throw end_of_buffer_exception{}; + } + val |= uint64_t(*p++) << shift; + } + + *data = reinterpret_cast(p); + return val; + } + +} // end namespace detail + +/** + * Decode a 64 bit varint. + * + * Strong exception guarantee: if there is an exception the data pointer will + * not be changed. + * + * @param[in,out] data Pointer to pointer to the input data. After the function + * returns this will point to the next data to be read. + * @param[in] end Pointer one past the end of the input data. + * @returns The decoded integer + * @throws varint_too_long_exception if the varint is longer then the maximum + * length that would fit in a 64 bit int. Usually this means your data + * is corrupted or you are trying to read something as a varint that + * isn't. + * @throws end_of_buffer_exception if the *end* of the buffer was reached + * before the end of the varint. + */ +inline uint64_t decode_varint(const char** data, const char* end) { + // If this is a one-byte varint, decode it here. + if (end != *data && ((static_cast(**data) & 0x80U) == 0)) { + const auto val = static_cast(**data); + ++(*data); + return val; + } + // If this varint is more than one byte, defer to complete implementation. + return detail::decode_varint_impl(data, end); +} + +/** + * Skip over a varint. + * + * Strong exception guarantee: if there is an exception the data pointer will + * not be changed. + * + * @param[in,out] data Pointer to pointer to the input data. After the function + * returns this will point to the next data to be read. + * @param[in] end Pointer one past the end of the input data. + * @throws end_of_buffer_exception if the *end* of the buffer was reached + * before the end of the varint. + */ +inline void skip_varint(const char** data, const char* end) { + const auto* begin = reinterpret_cast(*data); + const auto* iend = reinterpret_cast(end); + const int8_t* p = begin; + + while (p != iend && *p < 0) { + ++p; + } + + if (p - begin >= max_varint_length) { + throw varint_too_long_exception{}; + } + + if (p == iend) { + throw end_of_buffer_exception{}; + } + + ++p; + + *data = reinterpret_cast(p); +} + +/** + * Varint encode a 64 bit integer. + * + * @tparam T An output iterator type. + * @param data Output iterator the varint encoded value will be written to + * byte by byte. + * @param value The integer that will be encoded. + * @returns the number of bytes written + * @throws Any exception thrown by increment or dereference operator on data. + * @deprecated Use add_varint_to_buffer() instead. + */ +template +inline int write_varint(T data, uint64_t value) { + int n = 1; + + while (value >= 0x80U) { + *data++ = char((value & 0x7fU) | 0x80U); + value >>= 7U; + ++n; + } + *data = char(value); + + return n; +} + +/** + * Varint encode a 64 bit integer. + * + * @tparam TBuffer A buffer type. + * @param buffer Output buffer the varint will be written to. + * @param value The integer that will be encoded. + * @returns the number of bytes written + * @throws Any exception thrown by calling the buffer_push_back() function. + */ +template +inline void add_varint_to_buffer(TBuffer* buffer, uint64_t value) { + while (value >= 0x80U) { + buffer_customization::push_back(buffer, char((value & 0x7fU) | 0x80U)); + value >>= 7U; + } + buffer_customization::push_back(buffer, char(value)); +} + +/** + * Varint encode a 64 bit integer. + * + * @param data Where to add the varint. There must be enough space available! + * @param value The integer that will be encoded. + * @returns the number of bytes written + */ +inline int add_varint_to_buffer(char* data, uint64_t value) noexcept { + int n = 1; + + while (value >= 0x80U) { + *data++ = char((value & 0x7fU) | 0x80U); + value >>= 7U; + ++n; + } + *data = char(value); + + return n; +} + +/** + * Get the length of the varint the specified value would produce. + * + * @param value The integer to be encoded. + * @returns the number of bytes the varint would have if we created it. + */ +inline int length_of_varint(uint64_t value) noexcept { + int n = 1; + + while (value >= 0x80U) { + value >>= 7U; + ++n; + } + + return n; +} + +/** + * ZigZag encodes a 32 bit integer. + */ +inline constexpr uint32_t encode_zigzag32(int32_t value) noexcept { + return (static_cast(value) << 1U) ^ static_cast(-static_cast(static_cast(value) >> 31U)); +} + +/** + * ZigZag encodes a 64 bit integer. + */ +inline constexpr uint64_t encode_zigzag64(int64_t value) noexcept { + return (static_cast(value) << 1U) ^ static_cast(-static_cast(static_cast(value) >> 63U)); +} + +/** + * Decodes a 32 bit ZigZag-encoded integer. + */ +inline constexpr int32_t decode_zigzag32(uint32_t value) noexcept { + return static_cast((value >> 1U) ^ static_cast(-static_cast(value & 1U))); +} + +/** + * Decodes a 64 bit ZigZag-encoded integer. + */ +inline constexpr int64_t decode_zigzag64(uint64_t value) noexcept { + return static_cast((value >> 1U) ^ static_cast(-static_cast(value & 1U))); +} + +} // end namespace protozero + +#endif // PROTOZERO_VARINT_HPP diff --git a/include/protozero/version.hpp b/include/protozero/version.hpp new file mode 100644 index 00000000..fc9b9287 --- /dev/null +++ b/include/protozero/version.hpp @@ -0,0 +1,34 @@ +#ifndef PROTOZERO_VERSION_HPP +#define PROTOZERO_VERSION_HPP + +/***************************************************************************** + +protozero - Minimalistic protocol buffer decoder and encoder in C++. + +This file is from https://github.com/mapbox/protozero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file version.hpp + * + * @brief Contains macros defining the protozero version. + */ + +/// The major version number +#define PROTOZERO_VERSION_MAJOR 1 + +/// The minor version number +#define PROTOZERO_VERSION_MINOR 7 + +/// The patch number +#define PROTOZERO_VERSION_PATCH 1 + +/// The complete version number +#define PROTOZERO_VERSION_CODE (PROTOZERO_VERSION_MAJOR * 10000 + PROTOZERO_VERSION_MINOR * 100 + PROTOZERO_VERSION_PATCH) + +/// Version number as string +#define PROTOZERO_VERSION_STRING "1.7.1" + +#endif // PROTOZERO_VERSION_HPP diff --git a/include/read_pbf.h b/include/read_pbf.h index 94adb8e0..089f4f81 100644 --- a/include/read_pbf.h +++ b/include/read_pbf.h @@ -8,6 +8,7 @@ #include #include #include "osm_store.h" +#include "pbf_reader.h" // Protobuf #include "osmformat.pb.h" @@ -42,12 +43,12 @@ struct IndexedBlockMetadata: BlockMetadata { * * The output class is typically OsmMemTiles, which is derived from OsmLuaProcessing */ -class PbfReader +class PbfProcessor { public: enum class ReadPhase { Nodes = 1, Ways = 2, Relations = 4, RelationScan = 8 }; - PbfReader(OSMStore &osmStore); + PbfProcessor(OSMStore &osmStore); using pbfreader_generate_output = std::function< std::shared_ptr () >; using pbfreader_generate_stream = std::function< std::shared_ptr () >; @@ -66,12 +67,16 @@ class PbfReader // Read tags into a map from a way/node/relation using tag_map_t = boost::container::flat_map; template - void readTags(T &pbfObject, PrimitiveBlock const &pb, tag_map_t &tags) { - tags.reserve(pbfObject.keys_size()); - auto keysPtr = pbfObject.mutable_keys(); - auto valsPtr = pbfObject.mutable_vals(); - for (uint n=0; n < pbfObject.keys_size(); n++) { - tags[pb.stringtable().s(keysPtr->Get(n))] = pb.stringtable().s(valsPtr->Get(n)); + void readTags(T& pbfObject, const PbfReader::PrimitiveBlock& pb, tag_map_t& tags) { + tags.reserve(pbfObject.keys.size()); + // TODO: re-enable tags once we fifx lifetimes + for (uint n=0; n < pbfObject.keys.size(); n++) { + // TODO: tags should operate on data_view, not std::string + auto keyIndex = pbfObject.keys[n]; + auto valueIndex = pbfObject.vals[n]; + std::string key(pb.stringTable[keyIndex].data(), pb.stringTable[keyIndex].size()); + std::string value(pb.stringTable[valueIndex].data(), pb.stringTable[valueIndex].size()); + tags[key] = value; } } @@ -86,36 +91,36 @@ class PbfReader uint shard, uint effectiveShard ); - bool ReadNodes(OsmLuaProcessing &output, PrimitiveGroup &pg, PrimitiveBlock const &pb, const std::unordered_set &nodeKeyPositions); + bool ReadNodes(OsmLuaProcessing& output, PbfReader::PrimitiveGroup& pg, const PbfReader::PrimitiveBlock& pb, const std::unordered_set& nodeKeyPositions); bool ReadWays( - OsmLuaProcessing &output, - PrimitiveGroup &pg, - PrimitiveBlock const &pb, + OsmLuaProcessing& output, + PbfReader::PrimitiveGroup& pg, + const PbfReader::PrimitiveBlock& pb, bool locationsOnWays, uint shard, uint effectiveShards ); - bool ScanRelations(OsmLuaProcessing &output, PrimitiveGroup &pg, PrimitiveBlock const &pb); + bool ScanRelations(OsmLuaProcessing& output, PbfReader::PrimitiveGroup& pg, const PbfReader::PrimitiveBlock& pb); bool ReadRelations( OsmLuaProcessing& output, - PrimitiveGroup& pg, - const PrimitiveBlock& pb, + PbfReader::PrimitiveGroup& pg, + const PbfReader::PrimitiveBlock& pb, const BlockMetadata& blockMetadata, uint shard, uint effectiveShards ); - inline bool RelationIsType(Relation const &rel, int typeKey, int val) { - if (typeKey==-1 || val==-1) return false; - auto typeI = std::find(rel.keys().begin(), rel.keys().end(), typeKey); - if (typeI==rel.keys().end()) return false; - int typePos = typeI - rel.keys().begin(); - return rel.vals().Get(typePos) == val; + inline bool relationIsType(const PbfReader::Relation& rel, int typeKey, int val) { + if (typeKey == -1 || val == -1) return false; + auto typeI = std::find(rel.keys.begin(), rel.keys.end(), typeKey); + if (typeI == rel.keys.end()) return false; + int typePos = typeI - rel.keys.begin(); + return rel.vals[typePos] == val; } /// Find a string in the dictionary - static int findStringPosition(PrimitiveBlock const &pb, char const *str); + static int findStringPosition(const PbfReader::PrimitiveBlock& pb, const std::string& str); OSMStore &osmStore; std::mutex ioMutex; diff --git a/src/helpers.cpp b/src/helpers.cpp index 37d7a802..277b1223 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -10,7 +10,6 @@ #define MOD_GZIP_ZLIB_CFACTOR 9 #define MOD_GZIP_ZLIB_BSIZE 8096 -namespace geom = boost::geometry; using namespace std; // zlib routines from http://panthema.net/2007/0328-ZLibString.html @@ -64,7 +63,9 @@ std::string compress_string(const std::string& str, } // Decompress an STL string using zlib and return the original data. -std::string decompress_string(const std::string& str, bool asGzip) { +// The output buffer is passed in; callers are meant to re-use the buffer such +// that eventually no allocations are needed when decompressing. +void decompress_string(std::string& output, const char* input, uint32_t inputSize, bool asGzip) { z_stream zs; // z_stream is zlib's control structure memset(&zs, 0, sizeof(zs)); @@ -76,27 +77,27 @@ std::string decompress_string(const std::string& str, bool asGzip) { throw(std::runtime_error("inflateInit failed while decompressing.")); } - zs.next_in = (Bytef*)str.data(); - zs.avail_in = str.size(); + zs.next_in = (Bytef*)input; + zs.avail_in = inputSize; int ret; - char outbuffer[32768]; - std::string outstring; + + int actualOutputSize = 0; // get the decompressed bytes blockwise using repeated calls to inflate do { - zs.next_out = reinterpret_cast(outbuffer); - zs.avail_out = sizeof(outbuffer); + if (output.size() < actualOutputSize + 32768) + output.resize(actualOutputSize + 32768); - ret = inflate(&zs, 0); + zs.next_out = reinterpret_cast(&output[actualOutputSize]); + zs.avail_out = output.size() - actualOutputSize; - if (outstring.size() < zs.total_out) { - outstring.append(outbuffer, - zs.total_out - outstring.size()); - } + ret = inflate(&zs, 0); + actualOutputSize = zs.total_out; } while (ret == Z_OK); + output.resize(actualOutputSize); inflateEnd(&zs); if (ret != Z_STREAM_END) { // an error occurred that was not EOF @@ -105,8 +106,6 @@ std::string decompress_string(const std::string& str, bool asGzip) { << zs.msg; throw(std::runtime_error(oss.str())); } - - return outstring; } // Parse a Boost error diff --git a/src/pbf_blocks.cpp b/src/pbf_blocks.cpp index e33ffca0..32316dab 100644 --- a/src/pbf_blocks.cpp +++ b/src/pbf_blocks.cpp @@ -37,7 +37,8 @@ void readBlock(google::protobuf::Message *messagePtr, std::size_t datasize, istr readMessage(&blob, input, datasize); // Unzip the gzipped content - string contents = decompress_string(blob.zlib_data(), false); + string contents; + decompress_string(contents, blob.zlib_data().data(), blob.zlib_data().size(), false); messagePtr->ParseFromString(contents); } diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp new file mode 100644 index 00000000..0f49f899 --- /dev/null +++ b/src/pbf_reader.cpp @@ -0,0 +1,594 @@ +#include +#include +#include +#include "pbf_reader.h" +#include "helpers.h" + +using namespace PbfReader; + +// Where read_pbf.cpp has higher-level routines that populate our structures, +// pbf_reader.cpp has low-level tools that interact with the protobuf. +// +// WARNING: PbfReader adopts several constraints to optimize for tilemaker's +// use case. +// +// Objects returned from PbfReader can only be used on the same thread. +// The lifetime of an object is until the earlier of: +// - the thread calls a readXyz function at the same or higher level +// - e.g. readPrimitiveGroup invalidates the result of a prior readPrimitiveGroup call, +// but not the result of a prior readBlob call +// - the thread ends +// +// This allows us to re-use buffers to minimize heap churn and allocation cost. +// +// If you want to persist the data beyond that, you must make a copy in memory +// that you own. + +namespace PbfReader { + thread_local std::string blobStorage; // the blob as stored in the PBF + thread_local std::string blobStorage2; // the blob after decompression, if needed + thread_local PbfReader::PrimitiveBlock pb; + thread_local std::vector denseNodesIds; + thread_local std::vector denseNodesLons; + thread_local std::vector denseNodesLats; + thread_local std::vector denseNodesTagStart; + thread_local std::vector denseNodesTagEnd; + thread_local std::vector denseNodesKeyValues; + thread_local Way way; + thread_local Relation relation; + thread_local Nodes nodesImpl; +} + +BlobHeader PbfReader::readBlobHeader(std::istream& input) { + // See https://wiki.openstreetmap.org/wiki/PBF_Format#File_format + unsigned int size; + input.read((char*)&size, sizeof(size)); + if (input.eof()) { + return {"eof", -1}; + } + + endian_swap(size); + std::vector data; + data.resize(size); + input.read(&data[0], size); + + // TODO: check eofbit, failbit - https://cplusplus.com/reference/istream/istream/read/ + if (input.eof()) { + throw std::runtime_error("eof"); + } + + protozero::pbf_message message{&data[0], data.size()}; + + std::string type; + int32_t datasize = -1; + + while (message.next()) { + switch (message.tag()) { + case Schema::BlobHeader::required_string_type: + type = message.get_string(); + break; + case Schema::BlobHeader::required_int32_datasize: + datasize = message.get_int32(); + break; + default: + // ignore data for unknown tags to allow for future extensions + std::cout << "BlobHeader: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + message.skip(); + } + } + + if (type.empty()) + throw std::runtime_error("BlobHeader type is missing"); + + if (datasize == -1) + throw std::runtime_error("BlobHeader datasize is missing"); + + return { type, datasize }; +} + +protozero::data_view PbfReader::readBlob(int32_t datasize, std::istream& input) { + blobStorage.resize(datasize); + input.read(&blobStorage[0], datasize); + // TODO: check eofbit, failbit - https://cplusplus.com/reference/istream/istream/read/ + if (input.eof()) { + throw std::runtime_error("eof"); + } + + int32_t rawSize = -1; + protozero::data_view view; + protozero::pbf_message message{&blobStorage[0], blobStorage.size()}; + while (message.next()) { + switch (message.tag()) { + case Schema::Blob::optional_int32_raw_size: + rawSize = message.get_int32(); + break; + case Schema::Blob::oneof_data_bytes_raw: + view = message.get_view(); + break; + case Schema::Blob::oneof_data_bytes_zlib_data: + view = message.get_view(); + break; + default: + throw std::runtime_error("Blob: unknown tag: " + std::to_string(static_cast(message.tag()))); + } + } + + if (rawSize == -1) + // Data is not compressed, can return it directly. + return view; + + blobStorage2.resize(rawSize); + decompress_string(blobStorage2, view.data(), view.size(), false); + return { &blobStorage2[0], blobStorage2.size() }; +} + +HeaderBBox PbfReader::readHeaderBBox(protozero::data_view data) { + HeaderBBox box{0, 0, 0, 0}; + + protozero::pbf_message message{data}; + while (message.next()) { + switch (message.tag()) { + case Schema::HeaderBBox::required_sint64_left: + box.minLon = message.get_sint64() / 1000000000.0; + break; + case Schema::HeaderBBox::required_sint64_right: + box.maxLon = message.get_sint64() / 1000000000.0; + break; + case Schema::HeaderBBox::required_sint64_bottom: + box.minLat = message.get_sint64() / 1000000000.0; + break; + case Schema::HeaderBBox::required_sint64_top: + box.maxLat = message.get_sint64() / 1000000000.0; + break; + default: + throw std::runtime_error("HeaderBBox: unknown tag: " + std::to_string(static_cast(message.tag()))); + } + } + + return box; +} + +HeaderBlock PbfReader::readHeaderBlock(protozero::data_view data) { + HeaderBlock block{false}; + + protozero::pbf_message message{data}; + while (message.next()) { + switch (message.tag()) { + case Schema::HeaderBlock::optional_HeaderBBox_bbox: + block.hasBbox = true; + block.bbox = PbfReader::readHeaderBBox(message.get_view()); + break; + case Schema::HeaderBlock::repeated_string_optional_features: { + const auto feature = message.get_string(); + block.optionalFeatures.insert(feature); + break; + } + default: + // ignore data for unknown tags to allow for future extensions + //std::cout << "HeaderBlock: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + message.skip(); + } + } + + return block; +} + +void PbfReader::readStringTable(protozero::data_view data, std::vector& stringTable) { + protozero::pbf_message message{data}; + while (message.next()) { + switch (message.tag()) { + case Schema::StringTable::repeated_bytes_s: + stringTable.push_back(message.get_view()); + break; + default: + throw std::runtime_error("StringTable: unknown tag: " + std::to_string(static_cast(message.tag()))); + } + } +} + +PbfReader::PrimitiveBlock& PbfReader::readPrimitiveBlock(protozero::data_view data) { + pb.stringTable.clear(); + pb.internalGroups.clear(); + + protozero::pbf_message message{data}; + while (message.next()) { + switch (message.tag()) { + case Schema::PrimitiveBlock::required_StringTable_stringtable: + // Most of our use cases require the string table, so we eagerly + // initialize it. + PbfReader::readStringTable(message.get_view(), pb.stringTable); + break; + case Schema::PrimitiveBlock::repeated_PrimitiveGroup_primitivegroup: { + pb.internalGroups.push_back({message.get_view()}); + break; + } + default: + // ignore data for unknown tags to allow for future extensions + //std::cout << "HeaderBlock: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + message.skip(); + } + } + + pb.groupsImpl = PrimitiveBlock::PrimitiveGroups(pb.internalGroups); + + return pb; +} + +void PbfReader::readDenseNodes(protozero::data_view data) { + protozero::pbf_message message{data}; + + uint64_t id = 0; + int32_t lon = 0, lat = 0; + + while (message.next()) { + switch (message.tag()) { + case Schema::DenseNodes::repeated_sint64_id: { + auto pi = message.get_packed_sint64(); + for (auto i : pi) { + id += i; + denseNodesIds.push_back(id); + } + break; + } case Schema::DenseNodes::repeated_sint64_lat: { + auto pi = message.get_packed_sint64(); + for (auto i : pi) { + lat += i; + denseNodesLats.push_back(lat); + } + break; + } + case Schema::DenseNodes::repeated_sint64_lon: { + auto pi = message.get_packed_sint64(); + for (auto i : pi) { + lon += i; + denseNodesLons.push_back(lon); + } + break; + } + case Schema::DenseNodes::repeated_int32_keys_vals: { + auto pi = message.get_packed_int32(); + for (auto kv : pi) { + denseNodesKeyValues.push_back(kv); + } + break; + } + + default: + // ignore data for unknown tags to allow for future extensions + //std::cout << "HeaderBlock: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + message.skip(); + } + } + + for (uint32_t cur = 0, prev = 0; cur < denseNodesKeyValues.size(); cur++) { + if (denseNodesKeyValues[cur] == 0) { + denseNodesTagStart.push_back(prev); + denseNodesTagEnd.push_back(cur); + prev = cur + 1; + } + } + + while(denseNodesTagStart.size() < denseNodesIds.size()) { + denseNodesTagStart.push_back(0); + denseNodesTagEnd.push_back(0); + } +} + +PbfReader::PrimitiveGroup::PrimitiveGroup(protozero::data_view data): data(data), denseNodesInitialized(false) { +} + +int32_t PbfReader::PrimitiveGroup::translateNodeKeyValue(int32_t i) const { + return denseNodesKeyValues.at(i); +} + +protozero::data_view PbfReader::PrimitiveGroup::getDataView() { + return data; +} + +void PbfReader::PrimitiveGroup::ensureData() { + // Reset our thread locals. + denseNodesIds.clear(); + denseNodesLons.clear(); + denseNodesLats.clear(); + denseNodesTagStart.clear(); + denseNodesTagEnd.clear(); + denseNodesKeyValues.clear(); + internalWays.pg = this; + internalRelations.pg = this; + + protozero::pbf_message message{data}; + if (message.next()) { + switch (message.tag()) { + case Schema::PrimitiveGroup::repeated_Node_nodes: + throw std::runtime_error("PrimitiveGroup: non-dense Nodes are not supported"); + break; + case Schema::PrimitiveGroup::optional_DenseNodes_dense: + internalType = PrimitiveGroupType::DenseNodes; + readDenseNodes(message.get_view()); + break; + case Schema::PrimitiveGroup::repeated_Way_ways: + internalType = PrimitiveGroupType::Way; + break; + case Schema::PrimitiveGroup::repeated_Relation_relations: + internalType = PrimitiveGroupType::Relation; + break; + case Schema::PrimitiveGroup::repeated_ChangeSet_changesets: + internalType = PrimitiveGroupType::ChangeSet; + break; + default: + throw std::runtime_error("PrimitiveGroup: unknown tag: " + std::to_string(static_cast(message.tag()))); + } + } +} + +Nodes& PrimitiveGroup::nodes() const { return nodesImpl; }; +PrimitiveBlock::PrimitiveGroups& PrimitiveBlock::groups() { return groupsImpl; }; + +bool PbfReader::Nodes::Iterator::operator!=(Iterator& other) const { + return offset != other.offset; +} + +void PbfReader::Nodes::Iterator::operator++() { + offset++; + + if (offset < denseNodesIds.size()) { + node.id = denseNodesIds[offset]; + node.lon = denseNodesLons[offset]; + node.lat = denseNodesLats[offset]; + node.tagStart = denseNodesTagStart[offset]; + node.tagEnd = denseNodesTagEnd[offset]; + } +} + +PbfReader::Nodes::Node& PbfReader::Nodes::Iterator::operator*() { + return node; +} + + +PbfReader::Nodes::Iterator Nodes::begin() { + auto it = Iterator {-1}; + ++it; + return it; +} + +PbfReader::Nodes::Iterator Nodes::end() { + return Iterator {static_cast(denseNodesIds.size())}; +} + +bool PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator::operator!=(Iterator& other) const { + return offset != other.offset; +} +void PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator::operator++() { + offset++; + + if (offset < groups->size()) { + (*groups)[offset].ensureData(); + } +} +PrimitiveGroup& PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator::operator*() { + return (*groups)[offset]; +} +PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator PbfReader::PrimitiveBlock::PrimitiveGroups::begin() { + auto it = PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator {-1, *groups }; + ++it; + return it; +} +PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator PbfReader::PrimitiveBlock::PrimitiveGroups::end() { + return PrimitiveBlock::PrimitiveGroups::Iterator {static_cast(groups->size()), *groups }; +} + +PbfReader::PrimitiveGroupType PbfReader::PrimitiveGroup::type() const { + return internalType; +} + +void PbfReader::readWay(protozero::data_view data, Way& way) { + protozero::pbf_message message{data}; + + way.id = 0; + way.keys.clear(); + way.vals.clear(); + way.refs.clear(); + way.lats.clear(); + way.lons.clear(); + + uint64_t ref = 0; + uint32_t lat = 0, lon = 0; + + while (message.next()) { + switch (message.tag()) { + case Schema::Way::required_int64_id: + way.id = message.get_int64(); + break; + case Schema::Way::repeated_uint32_keys: { + auto pi = message.get_packed_uint32(); + for (auto i : pi) { + way.keys.push_back(i); + } + break; + } + case Schema::Way::repeated_uint32_vals: { + auto pi = message.get_packed_uint32(); + for (auto i : pi) { + way.vals.push_back(i); + } + break; + } + case Schema::Way::repeated_sint64_refs: { + auto pi = message.get_packed_sint64(); + for (auto i : pi) { + ref += i; + way.refs.push_back(ref); + } + break; + } + case Schema::Way::repeated_sint64_lats: { + auto pi = message.get_packed_sint64(); + for (auto i : pi) { + lat += i; + way.lats.push_back(lat); + } + break; + } + case Schema::Way::repeated_sint64_lons: { + auto pi = message.get_packed_sint64(); + for (auto i : pi) { + lon += i; + way.lons.push_back(lon); + } + break; + } + + default: + // ignore data for unknown tags to allow for future extensions + //std::cout << "Way: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + message.skip(); + } + } +} + +Ways& PbfReader::PrimitiveGroup::ways() const { + return internalWays; +} +bool PbfReader::Ways::Iterator::operator!=(PbfReader::Ways::Iterator& other) const { + return offset != other.offset; +} +void PbfReader::Ways::Iterator::operator++() { + if (message.next()) { + readWay(message.get_view(), way); + offset++; + } else { + offset = -1; + } +} +PbfReader::Way& PbfReader::Ways::Iterator::operator*() { + return way; +} +PbfReader::Ways::Iterator PbfReader::Ways::begin() { + if (pg->type() != PrimitiveGroupType::Way) + return Ways::Iterator{protozero::pbf_message{nullptr, 0}, 0}; + + protozero::pbf_message message{pg->getDataView()}; + if (message.next()) { + protozero::pbf_message message{pg->getDataView()}; + auto it = Ways::Iterator{message, -1}; + ++it; + return it; + } + + return Ways::Iterator{message, -1}; +} +PbfReader::Ways::Iterator PbfReader::Ways::end() { + if (pg->type() != PrimitiveGroupType::Way) + return Ways::Iterator{protozero::pbf_message{nullptr, 0}, 0}; + + return Ways::Iterator{protozero::pbf_message{nullptr, 0}, -1}; +} + +void PbfReader::readRelation(protozero::data_view data, Relation& relation) { + protozero::pbf_message message{data}; + + relation.id = 0; + relation.keys.clear(); + relation.vals.clear(); + relation.memids.clear(); + relation.roles_sid.clear(); + relation.types.clear(); + + uint64_t memid = 0; + + while (message.next()) { + switch (message.tag()) { + case Schema::Relation::required_int64_id: + relation.id = message.get_int64(); + break; + case Schema::Relation::repeated_uint32_keys: { + auto pi = message.get_packed_uint32(); + for (auto i : pi) { + relation.keys.push_back(i); + } + break; + } + case Schema::Relation::repeated_uint32_vals: { + auto pi = message.get_packed_uint32(); + for (auto i : pi) { + relation.vals.push_back(i); + } + break; + } + case Schema::Relation::repeated_int32_roles_sid: { + auto pi = message.get_packed_int32(); + for (auto i : pi) { + relation.roles_sid.push_back(i); + } + break; + } + case Schema::Relation::repeated_sint64_memids: { + auto pi = message.get_packed_sint64(); + for (auto i : pi) { + memid += i; + relation.memids.push_back(memid); + } + break; + } + case Schema::Relation::repeated_MemberType_types: { + auto pi = message.get_packed_int32(); + for (auto i : pi) { + relation.types.push_back(i); + } + break; + } + + default: + // ignore data for unknown tags to allow for future extensions + //std::cout << "Way: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + message.skip(); + } + } +} + +Relations& PbfReader::PrimitiveGroup::relations() const { + return internalRelations; +} +bool PbfReader::Relations::Iterator::operator!=(PbfReader::Relations::Iterator& other) const { + return offset != other.offset; +} +void PbfReader::Relations::Iterator::operator++() { + if (message.next()) { + readRelation(message.get_view(), relation); + offset++; + } else { + offset = -1; + } +} +PbfReader::Relation& PbfReader::Relations::Iterator::operator*() { + return relation; +} +PbfReader::Relations::Iterator PbfReader::Relations::begin() { + if (pg->type() != PrimitiveGroupType::Relation) + return Relations::Iterator{protozero::pbf_message{nullptr, 0}, 0}; + + protozero::pbf_message message{pg->getDataView()}; + if (message.next()) { + protozero::pbf_message message{pg->getDataView()}; + auto it = Relations::Iterator{message, -1}; + ++it; + return it; + } + + return Relations::Iterator{message, -1}; +} +PbfReader::Relations::Iterator PbfReader::Relations::end() { + if (pg->type() != PrimitiveGroupType::Relation) + return Relations::Iterator{protozero::pbf_message{nullptr, 0}, 0}; + + return Relations::Iterator{protozero::pbf_message{nullptr, 0}, -1}; +} + +HeaderBlock PbfReader::readHeaderFromFile(std::istream& input) { + PbfReader::BlobHeader bh = PbfReader::readBlobHeader(input); + protozero::data_view blob = PbfReader::readBlob(bh.datasize, input); + PbfReader::HeaderBlock header = PbfReader::readHeaderBlock(blob); + + return header; +} + diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 9b8b2f15..ed38c771 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -1,8 +1,8 @@ #include #include "read_pbf.h" #include "pbf_blocks.h" +#include "pbf_reader.h" -#include #include #include #include @@ -18,170 +18,171 @@ const std::string OptionSortTypeThenID = "Sort.Type_then_ID"; const std::string OptionLocationsOnWays = "LocationsOnWays"; std::atomic blocksProcessed(0), blocksToProcess(0); -PbfReader::PbfReader(OSMStore &osmStore) +PbfProcessor::PbfProcessor(OSMStore &osmStore) : osmStore(osmStore) { } -bool PbfReader::ReadNodes(OsmLuaProcessing &output, PrimitiveGroup &pg, PrimitiveBlock const &pb, const unordered_set &nodeKeyPositions) +bool PbfProcessor::ReadNodes(OsmLuaProcessing& output, PbfReader::PrimitiveGroup& pg, const PbfReader::PrimitiveBlock& pb, const unordered_set& nodeKeyPositions) { // ---- Read nodes + std::vector nodes; - if (pg.has_dense()) { - int64_t nodeId = 0; - int lon = 0; - int lat = 0; - int kvPos = 0; - DenseNodes dense = pg.dense(); - - std::vector nodes; - for (int j=0; j0) { - while (dense.keys_vals(kvPos)>0) { - if (nodeKeyPositions.find(dense.keys_vals(kvPos)) != nodeKeyPositions.end()) { - significant = true; - } - kvPos+=2; - } - kvPos++; + bool hadNodes = false; + for (auto& node : pg.nodes()) { + hadNodes = true; + + NodeID nodeId = node.id; + LatpLon latplon = { int(lat2latp(double(node.lat)/10000000.0)*10000000.0), node.lon }; + + bool significant = false; + // TODO: re-enable this + for (int i = node.tagStart; i < node.tagEnd; i += 2) { + auto keyIndex = pg.translateNodeKeyValue(i); + + if (nodeKeyPositions.find(keyIndex) != nodeKeyPositions.end()) { + significant = true; } + } - nodes.push_back(std::make_pair(static_cast(nodeId), node)); + nodes.push_back(std::make_pair(static_cast(nodeId), latplon)); - if (significant) { - // For tagged nodes, call Lua, then save the OutputObject - boost::container::flat_map tags; - tags.reserve(kvPos / 2); + if (significant) { + // For tagged nodes, call Lua, then save the OutputObject + boost::container::flat_map tags; + tags.reserve((node.tagEnd - node.tagStart) / 2); - for (uint n=kvStart; n(nodeId), node, tags); - } + // TODO: re-enable tags once we fix lifetimes of things + for (int n = node.tagStart; n < node.tagEnd; n += 2) { + auto keyIndex = pg.translateNodeKeyValue(n); + auto valueIndex = pg.translateNodeKeyValue(n + 1); - } + // TODO: tags should operate on data_view, not std::string + std::string key(pb.stringTable[keyIndex].data(), pb.stringTable[keyIndex].size()); + std::string value(pb.stringTable[valueIndex].data(), pb.stringTable[valueIndex].size()); + tags[key] = value; + } + output.setNode(static_cast(nodeId), latplon, tags); + } + } + if (nodes.size() > 0) { osmStore.nodes.insert(nodes); - return true; } - return false; + + return hadNodes; } -bool PbfReader::ReadWays( +bool PbfProcessor::ReadWays( OsmLuaProcessing &output, - PrimitiveGroup &pg, - PrimitiveBlock const &pb, + PbfReader::PrimitiveGroup& pg, + const PbfReader::PrimitiveBlock& pb, bool locationsOnWays, uint shard, uint effectiveShards ) { // ---- Read ways + { + // TODO: make this less ugly + auto b = pg.ways().begin(); + auto e = pg.ways().end(); + if (!(b != e)) return false; + } - if (pg.ways_size() > 0) { - Way pbfWay; - - const bool wayStoreRequiresNodes = osmStore.ways.requiresNodes(); - - std::vector llWays; - std::vector>> nodeWays; - LatpLonVec llVec; - std::vector nodeVec; - - for (int j=0; j(pbfWay.id()); - if (wayId >= pow(2,42)) throw std::runtime_error("Way ID negative or too large: "+std::to_string(wayId)); - - // Assemble nodelist - if (locationsOnWays) { - int lat=0, lon=0; - llVec.reserve(pbfWay.lats_size()); - for (int k=0; k llWays; + std::vector>> nodeWays; + LatpLonVec llVec; + std::vector nodeVec; - if (k == 0 && effectiveShards > 1 && !osmStore.nodes.contains(shard, nodeId)) { - skipToNext = true; - break; - } + for (PbfReader::Way pbfWay : pg.ways()) { + llVec.clear(); + nodeVec.clear(); - try { - llVec.push_back(osmStore.nodes.at(static_cast(nodeId))); - nodeVec.push_back(nodeId); - } catch (std::out_of_range &err) { - if (osmStore.integrity_enforced()) throw err; - } - } + WayID wayId = static_cast(pbfWay.id); + if (wayId >= pow(2,42)) throw std::runtime_error("Way ID negative or too large: "+std::to_string(wayId)); - if (skipToNext) - continue; + // Assemble nodelist + if (locationsOnWays) { + llVec.reserve(pbfWay.lats.size()); + for (int k=0; k(pbfWay.id()), llVec, tags); - - // If we need it for later, store the way's coordinates in the global way store - if (emitted || osmStore.way_is_used(wayId)) { - if (wayStoreRequiresNodes) - nodeWays.push_back(std::make_pair(wayId, nodeVec)); - else - llWays.push_back(std::make_pair(wayId, WayStore::latplon_vector_t(llVec.begin(), llVec.end()))); + bool skipToNext = false; + + for (int k=0; k 1 && !osmStore.nodes.contains(shard, nodeId)) { + skipToNext = true; + break; } - } catch (std::out_of_range &err) { - // Way is missing a node? - cerr << endl << err.what() << endl; + try { + llVec.push_back(osmStore.nodes.at(static_cast(nodeId))); + nodeVec.push_back(nodeId); + } catch (std::out_of_range &err) { + if (osmStore.integrity_enforced()) throw err; + } } + if (skipToNext) + continue; } + if (llVec.empty()) continue; + + try { + tag_map_t tags; + readTags(pbfWay, pb, tags); + bool emitted = output.setWay(static_cast(pbfWay.id), llVec, tags); + + // If we need it for later, store the way's coordinates in the global way store + if (emitted || osmStore.way_is_used(wayId)) { + if (wayStoreRequiresNodes) + nodeWays.push_back(std::make_pair(wayId, nodeVec)); + else + llWays.push_back(std::make_pair(wayId, WayStore::latplon_vector_t(llVec.begin(), llVec.end()))); + } - if (wayStoreRequiresNodes) { - osmStore.ways.shard(shard).insertNodes(nodeWays); - } else { - osmStore.ways.shard(shard).insertLatpLons(llWays); + } catch (std::out_of_range &err) { + // Way is missing a node? + cerr << endl << err.what() << endl; } - return true; } - return false; + + if (wayStoreRequiresNodes) { + osmStore.ways.shard(shard).insertNodes(nodeWays); + } else { + osmStore.ways.shard(shard).insertLatpLons(llWays); + } + + return true; } -bool PbfReader::ScanRelations(OsmLuaProcessing &output, PrimitiveGroup &pg, PrimitiveBlock const &pb) { +bool PbfProcessor::ScanRelations(OsmLuaProcessing& output, PbfReader::PrimitiveGroup& pg, const PbfReader::PrimitiveBlock& pb) { // Scan relations to see which ways we need to save - if (pg.relations_size()==0) return false; + { + // TODO: make this less ugly + auto b = pg.relations().begin(); + auto e = pg.relations().end(); + if (!(b != e)) return false; + } int typeKey = findStringPosition(pb, "type"); int mpKey = findStringPosition(pb, "multipolygon"); - for (int j=0; j(pbfRelation.id()); + WayID relid = static_cast(pbfRelation.id); if (!isMultiPolygon) { if (output.canReadRelations()) { tag_map_t tags; @@ -190,10 +191,9 @@ bool PbfReader::ScanRelations(OsmLuaProcessing &output, PrimitiveGroup &pg, Prim } if (!isAccepted) continue; } - int64_t lastID = 0; - for (int n=0; n < pbfRelation.memids_size(); n++) { - lastID += pbfRelation.memids(n); - if (pbfRelation.types(n) != Relation_MemberType_WAY) { continue; } + for (int n=0; n < pbfRelation.memids.size(); n++) { + uint64_t lastID = pbfRelation.memids[n]; + if (pbfRelation.types[n] != PbfReader::Relation::MemberType::WAY) { continue; } if (lastID >= pow(2,42)) throw std::runtime_error("Way ID in relation "+std::to_string(relid)+" negative or too large: "+std::to_string(lastID)); osmStore.mark_way_used(static_cast(lastID)); if (isAccepted) { osmStore.relation_contains_way(relid, lastID); } @@ -202,79 +202,82 @@ bool PbfReader::ScanRelations(OsmLuaProcessing &output, PrimitiveGroup &pg, Prim return true; } -bool PbfReader::ReadRelations( +bool PbfProcessor::ReadRelations( OsmLuaProcessing& output, - PrimitiveGroup& pg, - const PrimitiveBlock& pb, + PbfReader::PrimitiveGroup& pg, + const PbfReader::PrimitiveBlock& pb, const BlockMetadata& blockMetadata, uint shard, uint effectiveShards ) { // ---- Read relations + { + // TODO: make this less ugly + auto b = pg.relations().begin(); + auto e = pg.relations().end(); + if (!(b != e)) return false; + } - if (pg.relations_size() > 0) { - std::vector relations; - - int typeKey = findStringPosition(pb, "type"); - int mpKey = findStringPosition(pb, "multipolygon"); - int boundaryKey = findStringPosition(pb, "boundary"); - int innerKey= findStringPosition(pb, "inner"); - int outerKey= findStringPosition(pb, "outer"); - if (typeKey >-1 && mpKey>-1) { - for (int j=0; j(lastID); - - if (firstWay && effectiveShards > 1 && !osmStore.ways.contains(shard, wayId)) { - skipToNext = true; - break; - } - if (firstWay) - firstWay = false; - (role == innerKey ? innerWayVec : outerWayVec).push_back(wayId); + std::vector relations; + + int typeKey = findStringPosition(pb, "type"); + int mpKey = findStringPosition(pb, "multipolygon"); + int boundaryKey = findStringPosition(pb, "boundary"); + int innerKey= findStringPosition(pb, "inner"); + int outerKey= findStringPosition(pb, "outer"); + if (typeKey >-1 && mpKey>-1) { + int j = -1; + for (PbfReader::Relation pbfRelation : pg.relations()) { + j++; + if (j % blockMetadata.chunks != blockMetadata.chunk) + continue; + + bool isMultiPolygon = relationIsType(pbfRelation, typeKey, mpKey); + bool isBoundary = relationIsType(pbfRelation, typeKey, boundaryKey); + if (!isMultiPolygon && !isBoundary && !output.canWriteRelations()) continue; + + // Read relation members + WayVec outerWayVec, innerWayVec; + bool isInnerOuter = isBoundary || isMultiPolygon; + bool skipToNext = false; + bool firstWay = true; + for (int n = 0; n < pbfRelation.memids.size(); n++) { + uint64_t lastID = pbfRelation.memids[n]; + if (pbfRelation.types[n] != PbfReader::Relation::MemberType::WAY) { continue; } + int32_t role = pbfRelation.roles_sid[n]; + if (role==innerKey || role==outerKey) isInnerOuter=true; + WayID wayId = static_cast(lastID); + + if (firstWay && effectiveShards > 1 && !osmStore.ways.contains(shard, wayId)) { + skipToNext = true; + break; } + if (firstWay) + firstWay = false; + (role == innerKey ? innerWayVec : outerWayVec).push_back(wayId); + } - if (skipToNext) - continue; + if (skipToNext) + continue; - try { - tag_map_t tags; - readTags(pbfRelation, pb, tags); - output.setRelation(pbfRelation.id(), outerWayVec, innerWayVec, tags, isMultiPolygon, isInnerOuter); + try { + tag_map_t tags; + readTags(pbfRelation, pb, tags); + output.setRelation(pbfRelation.id, outerWayVec, innerWayVec, tags, isMultiPolygon, isInnerOuter); - } catch (std::out_of_range &err) { - // Relation is missing a member? - cerr << endl << err.what() << endl; - } + } catch (std::out_of_range &err) { + // Relation is missing a member? + cerr << endl << err.what() << endl; } } - - osmStore.relations_insert_front(relations); - return true; } - return false; + + osmStore.relations_insert_front(relations); + return true; } // Returns true when block was completely handled, thus could be omited by another phases. -bool PbfReader::ReadBlock( +bool PbfProcessor::ReadBlock( std::istream& infile, OsmLuaProcessing& output, const BlockMetadata& blockMetadata, @@ -287,8 +290,8 @@ bool PbfReader::ReadBlock( { infile.seekg(blockMetadata.offset); - PrimitiveBlock pb; - readBlock(&pb, blockMetadata.length, infile); + protozero::data_view blob = PbfReader::readBlob(blockMetadata.length, infile); + PbfReader::PrimitiveBlock& pb = PbfReader::readPrimitiveBlock(blob); if (infile.eof()) { return true; } @@ -299,12 +302,14 @@ bool PbfReader::ReadBlock( // Read the string table, and pre-calculate the positions of valid node keys unordered_set nodeKeyPositions; for (auto it : nodeKeys) { - nodeKeyPositions.insert(findStringPosition(pb, it.c_str())); + //nodeKeyPositions.insert(findStringPosition(pb, it)); + auto rv = findStringPosition(pb, it); + nodeKeyPositions.insert(rv); } - for (int i=0; i 1) str << std::to_string(shard + 1) << "/" << std::to_string(effectiveShards) << " "; - str << "Block " << blocksProcessed.load() << "/" << blocksToProcess.load() << " ways " << pg.ways_size() << " relations " << pg.relations_size() << " "; + // TODO: revive showing the # of ways/relations? + str << "Block " << blocksProcessed.load() << "/" << blocksToProcess.load() << " "; std::cout << str.str(); std::cout.flush(); ioMutex.unlock(); @@ -371,7 +377,7 @@ bool PbfReader::ReadBlock( // In later case block would not be handled during this phase, and should be // read again in remaining phases. Thus we return false to indicate that the // block was not handled completelly. - if(read_groups != pb.primitivegroup_size()) { + if(read_groups != primitiveGroupSize) { return false; } @@ -383,22 +389,19 @@ bool PbfReader::ReadBlock( bool blockHasPrimitiveGroupSatisfying( std::istream& infile, const BlockMetadata block, - std::function test + std::function test ) { - PrimitiveBlock pb; - // We may have previously read to EOF, so clear the internal error state infile.clear(); infile.seekg(block.offset); - readBlock(&pb, block.length, infile); + protozero::data_view blob = PbfReader::readBlob(block.length, infile); + PbfReader::PrimitiveBlock pb = PbfReader::readPrimitiveBlock(blob); + if (infile.eof()) { throw std::runtime_error("blockHasPrimitiveGroupSatisfying got unexpected eof"); } - for (int i=0; i const& nodeKeys, @@ -422,14 +425,10 @@ int PbfReader::ReadPbfFile( // ---- Read PBF osmStore.clear(); - HeaderBlock block; - readBlock(&block, readHeader(*infile).datasize(), *infile); - bool locationsOnWays = false; - for (std::string option : block.optional_features()) { - if (option == OptionLocationsOnWays) { - std::cout << ".osm.pbf file has locations on ways" << std::endl; - locationsOnWays = true; - } + PbfReader::HeaderBlock block = PbfReader::readHeaderFromFile(*infile); + bool locationsOnWays = block.optionalFeatures.find(OptionLocationsOnWays) != block.optionalFeatures.end(); + if (locationsOnWays) { + std::cout << ".osm.pbf file has locations on ways" << std::endl; } std::map blocks; @@ -438,14 +437,14 @@ int PbfReader::ReadPbfFile( // its meant to be an opaque token useful only for seeking. size_t filesize = 0; while (true) { - BlobHeader bh = readHeader(*infile); - filesize += bh.datasize(); + PbfReader::BlobHeader bh = PbfReader::readBlobHeader(*infile); + filesize += bh.datasize; if (infile->eof()) { break; } - blocks[blocks.size()] = { (long int)infile->tellg(), bh.datasize(), true, true, true, 0, 1 }; - infile->seekg(bh.datasize(), std::ios_base::cur); + blocks[blocks.size()] = { (long int)infile->tellg(), bh.datasize, true, true, true, 0, 1 }; + infile->seekg(bh.datasize, std::ios_base::cur); } if (hasSortTypeThenID) { @@ -464,7 +463,11 @@ int PbfReader::ReadPbfFile( return blockHasPrimitiveGroupSatisfying( *infile, blocks[i], - [](const PrimitiveGroup&pg) { return pg.ways_size() > 0 || pg.relations_size() > 0; } + [](const PbfReader::PrimitiveGroup& pg) { + for(auto w : pg.ways()) return true; + for(auto r : pg.relations()) return true; + return false; + } ); } ); @@ -477,7 +480,10 @@ int PbfReader::ReadPbfFile( return blockHasPrimitiveGroupSatisfying( *infile, blocks[i], - [](const PrimitiveGroup&pg) { return pg.relations_size() > 0; } + [](const PbfReader::PrimitiveGroup& pg) { + for (auto r : pg.relations()) return true; + return false; + } ); } ); @@ -626,9 +632,9 @@ int PbfReader::ReadPbfFile( } // Find a string in the dictionary -int PbfReader::findStringPosition(PrimitiveBlock const &pb, char const *str) { - for (int i=0; i sortOrders = layers.getSortOrders(); if (!mapsplit) { @@ -393,7 +393,7 @@ int main(int argc, char* argv[]) { if (!infile) { cerr << "Couldn't open .pbf file " << inputFile << endl; return -1; } const bool hasSortTypeThenID = PbfHasOptionalFeature(inputFile, OptionSortTypeThenID); - int ret = pbfReader.ReadPbfFile( + int ret = pbfProcessor.ReadPbfFile( nodeStore->shards(), hasSortTypeThenID, nodeKeys, @@ -455,6 +455,8 @@ int main(int argc, char* argv[]) { } } + exit(1); + return 1; // TODO // ---- Write out data // If mapsplit, read list of tiles available @@ -484,7 +486,7 @@ int main(int argc, char* argv[]) { cout << "Reading tile " << srcZ << ": " << srcX << "," << srcY << " (" << (run+1) << "/" << runs << ")" << endl; vector pbf = mapsplitFile.readTile(srcZ,srcX,tmsY); - int ret = pbfReader.ReadPbfFile( + int ret = pbfProcessor.ReadPbfFile( nodeStore->shards(), false, nodeKeys, diff --git a/test/monaco.pbf b/test/monaco.pbf new file mode 100644 index 0000000000000000000000000000000000000000..6e6c31220d384304d0eb81dbb93e62335d00470e GIT binary patch literal 533899 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 z≺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ρ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�AH zbJ>zWSVn&sldW_sN3ooopQ=`R{b+SieFo4mIkSP;p0TvgR_UB&u%(Cd(nE9paFR`>jIb-#P3$~ie{iPQTt&{wD$mhb5NctL7COdCJ-(!f1wZ;l^Gt0 z=?F+p^5xzQ`4<+KXuI7q+HSf zoa*BmoRKHJh(`+?#n{zDA2W5+WXf}XZzA846z>r<-~(?(J46^v=EZi5gRgeBkJ?dH z*6}2gF`&`$gFQ5Omh!5HsZOHrc@$XUYoSyjMjtpj9nh)Kh;mKiQ&Xz zO%BKozROjw|5RqBm)EV8=mt`DY;uThYk)(%M6$2qO>4Rcif3sq-b%W7@2Px+R|FbG zAO|AzD?KpeuJkZu5DIyvQ@E1posz0E%5NR*!IP~=7GiggJ4FN- z!bO&96m@8tfm@Q1MigJaNMejKuQwjWQwg@IZ+4CtW9aF+u~OXrhKmj}<}MX+Afus( zODytB!<5swWI&y{KlhbiD2iy>@ZkPb8-_K*d>g*mquXzzZ)uo6oyHQS!wj9SN-iPu zHx8nj{Ip{#HhCVze?YN>6(t#o75v^LBZgTY{2n2W5R^Mb>03(bFd%1Ck&MBcWAdceU%s%6R4}3tHTRF;+9@^v8{f z=yT +#include +#include +#include "external/minunit.h" +#include "pbf_reader.h" + +MU_TEST(test_pbf_reader) { + std::string filename; + filename = "test/monaco.pbf"; +// filename = "/home/cldellow/Downloads/north-america-latest.osm.pbf"; +// filename = "/home/cldellow/Downloads/great-britain-latest.osm.pbf"; +// filename = "/home/cldellow/Downloads/nova-scotia-latest.osm.pbf"; + std::ifstream monaco(filename, std::ifstream::in); + + PbfReader::BlobHeader bh = PbfReader::readBlobHeader(monaco); + protozero::data_view blob = PbfReader::readBlob(bh.datasize, monaco); + PbfReader::HeaderBlock header = PbfReader::readHeaderBlock(blob); + + mu_check(header.hasBbox); + mu_check(header.optionalFeatures.size() == 1); + mu_check(header.optionalFeatures.find("Sort.Type_then_ID") != header.optionalFeatures.end()); + + mu_check(header.bbox.minLon == 7.409205); + mu_check(header.bbox.maxLon == 7.448637); + mu_check(header.bbox.minLat == 43.723350); + mu_check(header.bbox.maxLat == 43.751690); + + + bool foundNode = false, foundWay = false, foundRelation = false; + int blocks = 0, groups = 0, strings = 0, nodes = 0, ways = 0, relations = 0; + while (!monaco.eof()) { + bh = PbfReader::readBlobHeader(monaco); + if (bh.type == "eof") + break; + + + blocks++; + blob = PbfReader::readBlob(bh.datasize, monaco); + + PbfReader::PrimitiveBlock pb = PbfReader::readPrimitiveBlock(blob); + + for (const auto str : pb.stringTable) { + if (strings == 200) { + std::string s(str.data(), str.size()); + mu_check(s == "description:FR"); + } + strings++; + } + + for (const auto& group : pb.groups()) { + groups++; + for (const auto& node : group.nodes()) { + nodes++; + + if (node.id == 21911886) { + foundNode = true; + + bool foundHighwayCrossing = false; + + for (int i = node.tagStart; i < node.tagEnd; i += 2) { + const auto keyIndex = group.translateNodeKeyValue(i); + const auto valueIndex = group.translateNodeKeyValue(i + 1); + std::string key(pb.stringTable[keyIndex].data(), pb.stringTable[keyIndex].size()); + std::string value(pb.stringTable[valueIndex].data(), pb.stringTable[valueIndex].size()); + + if (key == "highway" && value == "crossing") + foundHighwayCrossing = true; + } + mu_check(foundHighwayCrossing); + } + } + + for (const auto& way : group.ways()) { + ways++; + + if (way.id == 4224978) { + foundWay = true; + + bool foundSportSoccer = false; + for (int i = 0; i < way.keys.size(); i++) { + std::string key(pb.stringTable[way.keys[i]].data(), pb.stringTable[way.keys[i]].size()); + std::string value(pb.stringTable[way.vals[i]].data(), pb.stringTable[way.vals[i]].size()); + + if (key == "sport" && value == "soccer") + foundSportSoccer = true; + } + mu_check(foundSportSoccer); + + mu_check(way.refs.size() == 5); + mu_check(way.refs[0] == 25178088); + mu_check(way.refs[2] == 25178045); + mu_check(way.refs[4] == 25178088); + } + } + + for (const auto& relation : group.relations()) { + relations++; + + if (relation.id == 1124039) { + foundRelation = true; + mu_check(relation.memids.size() == 17); + mu_check(relation.types.size() == 17); + mu_check(relation.roles_sid.size() == 17); + mu_check(relation.types[0] == PbfReader::Relation::MemberType::NODE); + mu_check(relation.types[2] == PbfReader::Relation::MemberType::WAY); + mu_check(relation.types[16] == PbfReader::Relation::MemberType::RELATION); + } + } + } + } + + //std::cout << blocks << " blocks, " << groups << " groups, " << nodes << " nodes, " << ways << " ways, " << relations << " relations" << std::endl; + + mu_check(foundNode); + mu_check(foundWay); + mu_check(foundRelation); + + mu_check(blocks == 6); + mu_check(groups == 6); + mu_check(strings == 8236); + mu_check(nodes == 30477); + mu_check(ways == 4825); + mu_check(relations == 285); +} + +MU_TEST_SUITE(test_suite_pbf_reader) { + MU_RUN_TEST(test_pbf_reader); +} + +int main() { + MU_RUN_SUITE(test_suite_pbf_reader); + MU_REPORT(); + return MU_EXIT_CODE; +} From 01d4aeb1a92e8956f92241717430fd6ad2d55362 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Thu, 21 Dec 2023 20:03:44 -0500 Subject: [PATCH 45/81] use protozero::data_view for tags This didn't provide much impact on runtime or memory use, unfortunately. Also use a batchSize of 1 for relations. Unlike nodes and ways, we're not building a store, and so don't need long runs of continuous blocks. This also hopefully decreases the odds of two slow blocks being assigned to the same worker. --- include/osm_lua_processing.h | 7 ++++--- include/osm_store.h | 9 ++++++++ include/read_pbf.h | 9 ++++---- src/osm_lua_processing.cpp | 10 ++++++--- src/read_pbf.cpp | 40 +++++++++++++++++++++++++++--------- 5 files changed, 54 insertions(+), 21 deletions(-) diff --git a/include/osm_lua_processing.h b/include/osm_lua_processing.h index b646bc2e..54a939a4 100644 --- a/include/osm_lua_processing.h +++ b/include/osm_lua_processing.h @@ -13,6 +13,7 @@ #include "shp_mem_tiles.h" #include "osm_mem_tiles.h" #include "helpers.h" +#include #include @@ -71,7 +72,7 @@ class OsmLuaProcessing { // ---- Data loading methods - using tag_map_t = boost::container::flat_map; + using tag_map_t = boost::container::flat_map; // Scan non-MP relation bool scanRelation(WayID id, const tag_map_t &tags); @@ -97,7 +98,7 @@ class OsmLuaProcessing { bool Holds(const std::string& key) const; // Get an OSM tag for a given key (or return empty string if none) - const std::string& Find(const std::string& key) const; + const std::string Find(const std::string& key) const; // ---- Spatial queries called from Lua @@ -258,7 +259,7 @@ class OsmLuaProcessing { class LayerDefinition &layers; std::vector> outputs; // All output objects that have been created - const boost::container::flat_map* currentTags; + const boost::container::flat_map* currentTags; std::vector finalizeOutputs(); diff --git a/include/osm_store.h b/include/osm_store.h index 11158bb2..b8607543 100644 --- a/include/osm_store.h +++ b/include/osm_store.h @@ -11,12 +11,21 @@ #include #include #include +#include extern bool verbose; class NodeStore; class WayStore; +// A comparator for data_view so it can be used in boost's flat_map +struct DataViewLessThan { + bool operator()(const protozero::data_view& a, const protozero::data_view& b) const { + return a < b; + } +}; + + // // Internal data structures. // diff --git a/include/read_pbf.h b/include/read_pbf.h index 089f4f81..a514d322 100644 --- a/include/read_pbf.h +++ b/include/read_pbf.h @@ -9,6 +9,7 @@ #include #include "osm_store.h" #include "pbf_reader.h" +#include // Protobuf #include "osmformat.pb.h" @@ -65,17 +66,15 @@ class PbfProcessor ); // Read tags into a map from a way/node/relation - using tag_map_t = boost::container::flat_map; + using tag_map_t = boost::container::flat_map; template void readTags(T& pbfObject, const PbfReader::PrimitiveBlock& pb, tag_map_t& tags) { tags.reserve(pbfObject.keys.size()); - // TODO: re-enable tags once we fifx lifetimes for (uint n=0; n < pbfObject.keys.size(); n++) { - // TODO: tags should operate on data_view, not std::string auto keyIndex = pbfObject.keys[n]; auto valueIndex = pbfObject.vals[n]; - std::string key(pb.stringTable[keyIndex].data(), pb.stringTable[keyIndex].size()); - std::string value(pb.stringTable[valueIndex].data(), pb.stringTable[valueIndex].size()); + protozero::data_view key = pb.stringTable[keyIndex]; + protozero::data_view value = pb.stringTable[valueIndex]; tags[key] = value; } } diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index a90c8b6a..74858067 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -127,10 +127,10 @@ bool OsmLuaProcessing::Holds(const string& key) const { } // Get an OSM tag for a given key (or return empty string if none) -const string& OsmLuaProcessing::Find(const string& key) const { +const string OsmLuaProcessing::Find(const string& key) const { auto it = currentTags->find(key); if(it == currentTags->end()) return EMPTY_STRING; - return it->second; + return std::string(it->second.data(), it->second.size()); } // ---- Spatial queries called from Lua @@ -572,7 +572,11 @@ bool OsmLuaProcessing::scanRelation(WayID id, const tag_map_t &tags) { } if (!relationAccepted) return false; - osmStore.store_relation_tags(id, tags); + boost::container::flat_map m; + for (const auto& i : tags) { + m[std::string(i.first.data(), i.first.size())] = std::string(i.second.data(), i.second.size()); + } + osmStore.store_relation_tags(id, m); return true; } diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index ed38c771..9ebbdcbe 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -17,6 +17,20 @@ using namespace std; const std::string OptionSortTypeThenID = "Sort.Type_then_ID"; const std::string OptionLocationsOnWays = "LocationsOnWays"; std::atomic blocksProcessed(0), blocksToProcess(0); +std::mutex pbfm; + +struct Counts { + Counts(): nodeBlocks(0), wayBlocks(0), relationBlocks(0) {} + ~Counts() { + std::lock_guard lock(pbfm); + std::cout << "nodes=" << nodeBlocks << ", ways=" << wayBlocks << ", relations=" << relationBlocks << std::endl; + } + + uint64_t nodeBlocks; + uint64_t wayBlocks; + uint64_t relationBlocks; +}; +thread_local Counts counts; PbfProcessor::PbfProcessor(OSMStore &osmStore) : osmStore(osmStore) @@ -35,7 +49,6 @@ bool PbfProcessor::ReadNodes(OsmLuaProcessing& output, PbfReader::PrimitiveGroup LatpLon latplon = { int(lat2latp(double(node.lat)/10000000.0)*10000000.0), node.lon }; bool significant = false; - // TODO: re-enable this for (int i = node.tagStart; i < node.tagEnd; i += 2) { auto keyIndex = pg.translateNodeKeyValue(i); @@ -48,17 +61,15 @@ bool PbfProcessor::ReadNodes(OsmLuaProcessing& output, PbfReader::PrimitiveGroup if (significant) { // For tagged nodes, call Lua, then save the OutputObject - boost::container::flat_map tags; + boost::container::flat_map tags; tags.reserve((node.tagEnd - node.tagStart) / 2); - // TODO: re-enable tags once we fix lifetimes of things for (int n = node.tagStart; n < node.tagEnd; n += 2) { auto keyIndex = pg.translateNodeKeyValue(n); auto valueIndex = pg.translateNodeKeyValue(n + 1); - // TODO: tags should operate on data_view, not std::string - std::string key(pb.stringTable[keyIndex].data(), pb.stringTable[keyIndex].size()); - std::string value(pb.stringTable[valueIndex].data(), pb.stringTable[valueIndex].size()); + protozero::data_view key{pb.stringTable[keyIndex].data(), pb.stringTable[keyIndex].size()}; + protozero::data_view value{pb.stringTable[valueIndex].data(), pb.stringTable[valueIndex].size()}; tags[key] = value; } output.setNode(static_cast(nodeId), latplon, tags); @@ -66,6 +77,7 @@ bool PbfProcessor::ReadNodes(OsmLuaProcessing& output, PbfReader::PrimitiveGroup } if (nodes.size() > 0) { + counts.nodeBlocks++; osmStore.nodes.insert(nodes); } @@ -88,6 +100,8 @@ bool PbfProcessor::ReadWays( if (!(b != e)) return false; } + counts.wayBlocks++; + const bool wayStoreRequiresNodes = osmStore.ways.requiresNodes(); std::vector llWays; @@ -218,6 +232,7 @@ bool PbfProcessor::ReadRelations( if (!(b != e)) return false; } + counts.relationBlocks++; std::vector relations; int typeKey = findStringPosition(pb, "type"); @@ -569,10 +584,15 @@ int PbfProcessor::ReadPbfFile( blocksToProcess = filteredBlocks.size(); blocksProcessed = 0; - // When processing blocks, we try to give each worker large batches - // of contiguous blocks, so that they might benefit from long runs - // of sorted indexes, and locality of nearby IDs. - const size_t batchSize = (filteredBlocks.size() / (threadNum * 8)) + 1; + // Relations have very non-uniform processing times, so prefer + // to process them as granularly as possible. + size_t batchSize = 1; + + // When creating NodeStore/WayStore, we try to give each worker + // large batches of contiguous blocks, so that they might benefit from + // long runs of sorted indexes, and locality of nearby IDs. + if (phase == ReadPhase::Nodes || phase == ReadPhase::Ways) + batchSize = (filteredBlocks.size() / (threadNum * 8)) + 1; size_t consumed = 0; auto it = filteredBlocks.begin(); From 5ab10dc0f774fd5f31ddbc2f6c03a8cedd422b36 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Thu, 21 Dec 2023 21:14:12 -0500 Subject: [PATCH 46/81] calculateCentroid: use multiPolygonCached() OSMStore::wayListMultiPolygon can be expensive for multipolygons that have many inners. For some multipolygons, the call to `boost::geometry::within` can take ~30 sec. --- src/osm_lua_processing.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index 74858067..c651394c 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -477,8 +477,7 @@ Point OsmLuaProcessing::calculateCentroid() { Point centroid; if (isRelation) { Geometry tmp; - tmp = osmStore.wayListMultiPolygon( - outerWayVecPtr->cbegin(), outerWayVecPtr->cend(), innerWayVecPtr->begin(), innerWayVecPtr->cend()); + tmp = multiPolygonCached(); geom::centroid(tmp, centroid); return Point(centroid.x()*10000000.0, centroid.y()*10000000.0); } else if (isWay) { From 1d48e4349c369886259b6a898bfd48584a9b5860 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Thu, 21 Dec 2023 21:51:50 -0500 Subject: [PATCH 47/81] remove timing --- src/read_pbf.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 9ebbdcbe..62c05f73 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -17,20 +17,6 @@ using namespace std; const std::string OptionSortTypeThenID = "Sort.Type_then_ID"; const std::string OptionLocationsOnWays = "LocationsOnWays"; std::atomic blocksProcessed(0), blocksToProcess(0); -std::mutex pbfm; - -struct Counts { - Counts(): nodeBlocks(0), wayBlocks(0), relationBlocks(0) {} - ~Counts() { - std::lock_guard lock(pbfm); - std::cout << "nodes=" << nodeBlocks << ", ways=" << wayBlocks << ", relations=" << relationBlocks << std::endl; - } - - uint64_t nodeBlocks; - uint64_t wayBlocks; - uint64_t relationBlocks; -}; -thread_local Counts counts; PbfProcessor::PbfProcessor(OSMStore &osmStore) : osmStore(osmStore) @@ -77,7 +63,6 @@ bool PbfProcessor::ReadNodes(OsmLuaProcessing& output, PbfReader::PrimitiveGroup } if (nodes.size() > 0) { - counts.nodeBlocks++; osmStore.nodes.insert(nodes); } @@ -100,8 +85,6 @@ bool PbfProcessor::ReadWays( if (!(b != e)) return false; } - counts.wayBlocks++; - const bool wayStoreRequiresNodes = osmStore.ways.requiresNodes(); std::vector llWays; @@ -232,7 +215,6 @@ bool PbfProcessor::ReadRelations( if (!(b != e)) return false; } - counts.relationBlocks++; std::vector relations; int typeKey = findStringPosition(pb, "type"); From 5abc57c136b3a952ae476bf92a2f094bf40a9fae Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Thu, 21 Dec 2023 22:22:05 -0500 Subject: [PATCH 48/81] add empty() function for OSM entities --- include/pbf_reader.h | 3 +++ src/pbf_reader.cpp | 21 +++++++++++++-------- src/read_pbf.cpp | 29 +++++++---------------------- src/tilemaker.cpp | 2 -- 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/include/pbf_reader.h b/include/pbf_reader.h index b6cb9482..d2faf44e 100644 --- a/include/pbf_reader.h +++ b/include/pbf_reader.h @@ -125,6 +125,7 @@ namespace PbfReader { }; Iterator begin(); Iterator end(); + bool empty(); }; struct Way { @@ -158,6 +159,7 @@ namespace PbfReader { }; Iterator begin(); Iterator end(); + bool empty(); PrimitiveGroup* pg; }; @@ -173,6 +175,7 @@ namespace PbfReader { }; Iterator begin(); Iterator end(); + bool empty(); PrimitiveGroup* pg; }; diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp index 0f49f899..b03dfe45 100644 --- a/src/pbf_reader.cpp +++ b/src/pbf_reader.cpp @@ -52,10 +52,8 @@ BlobHeader PbfReader::readBlobHeader(std::istream& input) { data.resize(size); input.read(&data[0], size); - // TODO: check eofbit, failbit - https://cplusplus.com/reference/istream/istream/read/ - if (input.eof()) { - throw std::runtime_error("eof"); - } + if (input.eof()) + throw std::runtime_error("readBlobHeader: unexpected eof"); protozero::pbf_message message{&data[0], data.size()}; @@ -89,10 +87,8 @@ BlobHeader PbfReader::readBlobHeader(std::istream& input) { protozero::data_view PbfReader::readBlob(int32_t datasize, std::istream& input) { blobStorage.resize(datasize); input.read(&blobStorage[0], datasize); - // TODO: check eofbit, failbit - https://cplusplus.com/reference/istream/istream/read/ - if (input.eof()) { - throw std::runtime_error("eof"); - } + if (input.eof()) + throw std::runtime_error("readBlob: unexpected eof"); int32_t rawSize = -1; protozero::data_view view; @@ -344,6 +340,9 @@ PbfReader::Nodes::Node& PbfReader::Nodes::Iterator::operator*() { return node; } +bool Nodes::empty() { + return denseNodesIds.empty(); +} PbfReader::Nodes::Iterator Nodes::begin() { auto it = Iterator {-1}; @@ -463,6 +462,9 @@ void PbfReader::Ways::Iterator::operator++() { PbfReader::Way& PbfReader::Ways::Iterator::operator*() { return way; } +bool PbfReader::Ways::empty() { + return pg->type() != PrimitiveGroupType::Way; +} PbfReader::Ways::Iterator PbfReader::Ways::begin() { if (pg->type() != PrimitiveGroupType::Way) return Ways::Iterator{protozero::pbf_message{nullptr, 0}, 0}; @@ -563,6 +565,9 @@ void PbfReader::Relations::Iterator::operator++() { PbfReader::Relation& PbfReader::Relations::Iterator::operator*() { return relation; } +bool PbfReader::Relations::empty() { + return pg->type() != PrimitiveGroupType::Relation; +} PbfReader::Relations::Iterator PbfReader::Relations::begin() { if (pg->type() != PrimitiveGroupType::Relation) return Relations::Iterator{protozero::pbf_message{nullptr, 0}, 0}; diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 62c05f73..3c436190 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -27,10 +27,7 @@ bool PbfProcessor::ReadNodes(OsmLuaProcessing& output, PbfReader::PrimitiveGroup // ---- Read nodes std::vector nodes; - bool hadNodes = false; for (auto& node : pg.nodes()) { - hadNodes = true; - NodeID nodeId = node.id; LatpLon latplon = { int(lat2latp(double(node.lat)/10000000.0)*10000000.0), node.lon }; @@ -66,7 +63,7 @@ bool PbfProcessor::ReadNodes(OsmLuaProcessing& output, PbfReader::PrimitiveGroup osmStore.nodes.insert(nodes); } - return hadNodes; + return !pg.nodes().empty(); } bool PbfProcessor::ReadWays( @@ -78,12 +75,8 @@ bool PbfProcessor::ReadWays( uint effectiveShards ) { // ---- Read ways - { - // TODO: make this less ugly - auto b = pg.ways().begin(); - auto e = pg.ways().end(); - if (!(b != e)) return false; - } + if (pg.ways().empty()) + return false; const bool wayStoreRequiresNodes = osmStore.ways.requiresNodes(); @@ -166,12 +159,8 @@ bool PbfProcessor::ReadWays( bool PbfProcessor::ScanRelations(OsmLuaProcessing& output, PbfReader::PrimitiveGroup& pg, const PbfReader::PrimitiveBlock& pb) { // Scan relations to see which ways we need to save - { - // TODO: make this less ugly - auto b = pg.relations().begin(); - auto e = pg.relations().end(); - if (!(b != e)) return false; - } + if (pg.relations().empty()) + return false; int typeKey = findStringPosition(pb, "type"); int mpKey = findStringPosition(pb, "multipolygon"); @@ -208,12 +197,8 @@ bool PbfProcessor::ReadRelations( uint effectiveShards ) { // ---- Read relations - { - // TODO: make this less ugly - auto b = pg.relations().begin(); - auto e = pg.relations().end(); - if (!(b != e)) return false; - } + if (pg.relations().empty()) + return false; std::vector relations; diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index c1dd842b..262fc82d 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -455,8 +455,6 @@ int main(int argc, char* argv[]) { } } - exit(1); - return 1; // TODO // ---- Write out data // If mapsplit, read list of tiles available From 4130f513c55d79943dff48232fd038800a1314d8 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Fri, 22 Dec 2023 00:03:02 -0500 Subject: [PATCH 49/81] extend --materialize-geometries to LayerAsCentroid It turns out that about 20% of LayerAsCentroid calls are for nodes, which this branch could already do. The remaining calls are predominantly ways, e.g. housenumbers. We always materialize relation centroids, as they're expensive to compute. In GB, this saves about 6.4M points, ~102M. Scaled to the planet, it's perhaps a 4.5GB savings, which should let us use a more aggressive shard strategy. It seems to add 3-4 seconds to the time to process GB. --- include/osm_mem_tiles.h | 18 +++++++++--------- include/tile_data.h | 12 +++++++----- src/osm_lua_processing.cpp | 16 +++++++++++++++- src/osm_mem_tiles.cpp | 25 ++++++++++++++----------- src/tile_data.cpp | 30 ++++++++++-------------------- src/tile_worker.cpp | 2 +- 6 files changed, 56 insertions(+), 47 deletions(-) diff --git a/include/osm_mem_tiles.h b/include/osm_mem_tiles.h index e7aff7ee..3c920b08 100644 --- a/include/osm_mem_tiles.h +++ b/include/osm_mem_tiles.h @@ -9,12 +9,12 @@ // NB: Currently, USE_NODE_STORE and USE_WAY_STORE are equivalent. // If we permit LayerAsCentroid to be generated from the OSM stores, // this will have to change. -#define OSM_THRESHOLD (1ull << 35) -#define USE_NODE_STORE (1ull << 35) -#define IS_NODE(x) (((x) >> 35) == (USE_NODE_STORE >> 35)) -#define USE_WAY_STORE (1ull << 35) -#define IS_WAY(x) (((x) >> 35) == (USE_WAY_STORE >> 35)) -#define OSM_ID(x) ((x) & 0b11111111111111111111111111111111111) +#define OSM_THRESHOLD (1ull << TILE_DATA_ID_SIZE) +#define USE_NODE_STORE (2ull << TILE_DATA_ID_SIZE) +#define IS_NODE(x) (((x) >> TILE_DATA_ID_SIZE) == (USE_NODE_STORE >> TILE_DATA_ID_SIZE)) +#define USE_WAY_STORE (1ull << TILE_DATA_ID_SIZE) +#define IS_WAY(x) (((x) >> TILE_DATA_ID_SIZE) == (USE_WAY_STORE >> TILE_DATA_ID_SIZE)) +#define OSM_ID(x) ((x) & 0b1111111111111111111111111111111111) class NodeStore; class WayStore; @@ -44,14 +44,14 @@ class OsmMemTiles : public TileDataSource { const NodeID objectID, const TileBbox &bbox ) override; - LatpLon buildNodeGeometry(OutputGeometryType const geomType, NodeID const objectID, const TileBbox &bbox) const override; + LatpLon buildNodeGeometry(NodeID const objectID, const TileBbox &bbox) const override; void Clear(); private: - void populateLinestring(Linestring& ls, NodeID objectID); - Linestring& getOrBuildLinestring(NodeID objectID); + void populateLinestring(Linestring& ls, NodeID objectID) const; + Linestring& getOrBuildLinestring(NodeID objectID) const; void populateMultiPolygon(MultiPolygon& dst, NodeID objectID) override; const NodeStore& nodeStore; diff --git a/include/tile_data.h b/include/tile_data.h index 2c5fe4df..6b59ee3f 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -12,6 +12,8 @@ #include "clip_cache.h" #include "mmap_allocator.h" +#define TILE_DATA_ID_SIZE 34 + typedef std::vector SourceList; class TileBbox; @@ -407,7 +409,7 @@ class TileDataSource { ); virtual Geometry buildWayGeometry(OutputGeometryType const geomType, NodeID const objectID, const TileBbox &bbox); - virtual LatpLon buildNodeGeometry(OutputGeometryType const geomType, NodeID const objectID, const TileBbox &bbox) const; + virtual LatpLon buildNodeGeometry(NodeID const objectID, const TileBbox &bbox) const; void open() { // Put something at index 0 of all stores so that 0 can be used @@ -425,18 +427,18 @@ class TileDataSource { NodeID storePoint(Point const &input); inline size_t getShard(NodeID id) const { - // Note: we only allocate 35 bits for the IDs. This allows us to - // use bit 36 for TileDataSource-specific handling (e.g., + // Note: we only allocate 34 bits for the IDs. This allows us to + // use bits 35 and 36 for TileDataSource-specific handling (e.g., // OsmMemTiles may want to generate points/ways on the fly by // referring to the WayStore). - return id >> (35 - shardBits); + return id >> (TILE_DATA_ID_SIZE - shardBits); } virtual void populateMultiPolygon(MultiPolygon& dst, NodeID objectID); inline size_t getId(NodeID id) const { - return id & (~(~0ull << (35 - shardBits))); + return id & (~(~0ull << (TILE_DATA_ID_SIZE - shardBits))); } const Point& retrievePoint(NodeID id) const { diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index a90c8b6a..faf69ec7 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -468,7 +468,21 @@ void OsmLuaProcessing::LayerAsCentroid(const string &layerName) { return; } - NodeID id = osmMemTiles.storePoint(geomp); + NodeID id = 0; + // We don't do lazy centroids for relations - calculating their centroid + // can be quite expensive, and there's not as many of them as there are + // ways. + if (materializeGeometries || isRelation) { + id = osmMemTiles.storePoint(geomp); + } else if (!isRelation && !isWay) { + // Sometimes people call LayerAsCentroid(...) on a node, because they're + // writing a generic handler that doesn't know if it's a node or a way, + // e.g. POIs. + id = USE_NODE_STORE | originalOsmID; + } else { + id = USE_WAY_STORE | originalOsmID; + wayEmitted = true; + } OutputObject oo(POINT_, layers.layerMap[layerName], id, 0, layerMinZoom); outputs.push_back(std::make_pair(std::move(oo), attributes)); } diff --git a/src/osm_mem_tiles.cpp b/src/osm_mem_tiles.cpp index 5cfc3c3d..7dc03f45 100644 --- a/src/osm_mem_tiles.cpp +++ b/src/osm_mem_tiles.cpp @@ -19,24 +19,27 @@ OsmMemTiles::OsmMemTiles( } LatpLon OsmMemTiles::buildNodeGeometry( - OutputGeometryType const geomType, NodeID const objectID, const TileBbox &bbox ) const { if (objectID < OSM_THRESHOLD) { - return TileDataSource::buildNodeGeometry(geomType, objectID, bbox); + return TileDataSource::buildNodeGeometry(objectID, bbox); } - switch(geomType) { - case POINT_: { - return nodeStore.at(OSM_ID(objectID)); - } + if (IS_NODE(objectID)) + return nodeStore.at(OSM_ID(objectID)); + - default: - break; + if (IS_WAY(objectID)) { + Linestring& ls = getOrBuildLinestring(objectID); + Point centroid; + Polygon p; + geom::assign_points(p, ls); + geom::centroid(p, centroid); + return LatpLon{(int32_t)(centroid.y()*10000000.0), (int32_t)(centroid.x()*10000000.0)}; } - throw std::runtime_error("Geometry type is not point"); + throw std::runtime_error("OsmMemTiles::buildNodeGeometry: unsupported objectID"); } Geometry OsmMemTiles::buildWayGeometry( @@ -79,7 +82,7 @@ Geometry OsmMemTiles::buildWayGeometry( throw std::runtime_error("buildWayGeometry: unexpected objectID: " + std::to_string(objectID)); } -void OsmMemTiles::populateLinestring(Linestring& ls, NodeID objectID) { +void OsmMemTiles::populateLinestring(Linestring& ls, NodeID objectID) const { std::vector nodes = wayStore.at(OSM_ID(objectID)); for (const LatpLon& node : nodes) { @@ -87,7 +90,7 @@ void OsmMemTiles::populateLinestring(Linestring& ls, NodeID objectID) { } } -Linestring& OsmMemTiles::getOrBuildLinestring(NodeID objectID) { +Linestring& OsmMemTiles::getOrBuildLinestring(NodeID objectID) const { // Note: this function returns a reference, not a shared_ptr. // // This is safe, because this function is the only thing that can diff --git a/src/tile_data.cpp b/src/tile_data.cpp index 8a8053bf..f78bbdda 100644 --- a/src/tile_data.cpp +++ b/src/tile_data.cpp @@ -339,22 +339,12 @@ Geometry TileDataSource::buildWayGeometry(OutputGeometryType const geomType, } } -LatpLon TileDataSource::buildNodeGeometry(OutputGeometryType const geomType, - NodeID const objectID, const TileBbox &bbox) const { - switch(geomType) { - case POINT_: { - auto p = retrievePoint(objectID); - LatpLon out; - out.latp = p.y(); - out.lon = p.x(); - return out; - } - - default: - break; - } - - throw std::runtime_error("Geometry type is not point"); +LatpLon TileDataSource::buildNodeGeometry(NodeID const objectID, const TileBbox &bbox) const { + auto p = retrievePoint(objectID); + LatpLon out; + out.latp = p.y(); + out.lon = p.x(); + return out; } @@ -538,7 +528,7 @@ NodeID TileDataSource::storePoint(const Point& input) { NodeID offset = store.second->size(); store.second->emplace_back(input); - NodeID rv = (store.first << (35 - shardBits)) + offset; + NodeID rv = (store.first << (TILE_DATA_ID_SIZE - shardBits)) + offset; return rv; } @@ -548,7 +538,7 @@ NodeID TileDataSource::storeLinestring(const Linestring& src) { NodeID offset = store.second->size(); store.second->emplace_back(std::move(dst)); - NodeID rv = (store.first << (35 - shardBits)) + offset; + NodeID rv = (store.first << (TILE_DATA_ID_SIZE - shardBits)) + offset; return rv; } @@ -570,7 +560,7 @@ NodeID TileDataSource::storeMultiPolygon(const MultiPolygon& src) { NodeID offset = store.second->size(); store.second->emplace_back(std::move(dst)); - NodeID rv = (store.first << (35 - shardBits)) + offset; + NodeID rv = (store.first << (TILE_DATA_ID_SIZE - shardBits)) + offset; return rv; } @@ -585,7 +575,7 @@ NodeID TileDataSource::storeMultiLinestring(const MultiLinestring& src) { NodeID offset = store.second->size(); store.second->emplace_back(std::move(dst)); - NodeID rv = (store.first << (35 - shardBits)) + offset; + NodeID rv = (store.first << (TILE_DATA_ID_SIZE - shardBits)) + offset; return rv; } diff --git a/src/tile_worker.cpp b/src/tile_worker.cpp index 07d8320a..d59e7fef 100644 --- a/src/tile_worker.cpp +++ b/src/tile_worker.cpp @@ -176,7 +176,7 @@ void ProcessObjects( if (oo.oo.geomType == POINT_) { vector_tile::Tile_Feature *featurePtr = vtLayer->add_features(); - LatpLon pos = source->buildNodeGeometry(oo.oo.geomType, oo.oo.objectID, bbox); + LatpLon pos = source->buildNodeGeometry(oo.oo.objectID, bbox); featurePtr->add_geometry(9); // moveTo, repeat x1 pair xy = bbox.scaleLatpLon(pos.latp/10000000.0, pos.lon/10000000.0); featurePtr->add_geometry((xy.first << 1) ^ (xy.first >> 31)); From d6d3f0ee3f86cf63eaf7e6060c932c4c9ea2c819 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 23 Dec 2023 17:26:55 -0500 Subject: [PATCH 50/81] add `DequeMap`, change AttributeStore to use it This implements the idea in https://github.com/systemed/tilemaker/issues/622#issuecomment-1866813888 Rather than storing a `deque` and a `flat_map`, store a `deque` and `vector`, to save 8 bytes per AttributePair and AttributeSet. --- Makefile | 6 ++ include/attribute_store.h | 93 +++++++++++------------- include/deque_map.h | 128 ++++++++++++++++++++++++++++++++++ src/attribute_store.cpp | 79 +++++++++------------ test/attribute_store.test.cpp | 1 - test/deque_map.test.cpp | 63 +++++++++++++++++ 6 files changed, 272 insertions(+), 98 deletions(-) create mode 100644 include/deque_map.h create mode 100644 test/deque_map.test.cpp diff --git a/Makefile b/Makefile index 81779d79..0cdd9935 100644 --- a/Makefile +++ b/Makefile @@ -130,6 +130,7 @@ tilemaker: \ test: \ test_append_vector \ test_attribute_store \ + test_deque_map \ test_pooled_string \ test_sorted_node_store \ test_sorted_way_store @@ -146,6 +147,11 @@ test_attribute_store: \ test/attribute_store.test.o $(CXX) $(CXXFLAGS) -o test.attribute_store $^ $(INC) $(LIB) $(LDFLAGS) && ./test.attribute_store +test_deque_map: \ + test/deque_map.test.o + $(CXX) $(CXXFLAGS) -o test.deque_map $^ $(INC) $(LIB) $(LDFLAGS) && ./test.deque_map + + test_pooled_string: \ src/mmap_allocator.o \ src/pooled_string.o \ diff --git a/include/attribute_store.h b/include/attribute_store.h index 194f62f6..3aea19cf 100644 --- a/include/attribute_store.h +++ b/include/attribute_store.h @@ -11,6 +11,7 @@ #include #include #include "pooled_string.h" +#include "deque_map.h" /* AttributeStore - global dictionary for attributes */ @@ -90,6 +91,19 @@ struct AttributePair { return *this; } + bool operator<(const AttributePair& other) const { + if (minzoom != other.minzoom) + return minzoom < other.minzoom; + if (keyIndex != other.keyIndex) + return keyIndex < other.keyIndex; + if (valueType != other.valueType) return valueType < other.valueType; + + if (hasStringValue()) return pooledString() < other.pooledString(); + if (hasBoolValue()) return boolValue() < other.boolValue(); + if (hasFloatValue()) return floatValue() < other.floatValue(); + throw std::runtime_error("Invalid type in attribute store"); + } + bool operator==(const AttributePair &other) const { if (minzoom!=other.minzoom || keyIndex!=other.keyIndex || valueType!=other.valueType) return false; if (valueType == AttributePairType::String) @@ -173,16 +187,14 @@ class AttributePairStore { public: AttributePairStore(): finalized(false), - pairs(ATTRIBUTE_SHARDS), - pairsMaps(ATTRIBUTE_SHARDS), - pairsMutex(ATTRIBUTE_SHARDS), - hotShardSize(0) + pairsMutex(ATTRIBUTE_SHARDS) { - // NB: the hot shard is stored in its own, pre-allocated vector. - // pairs[0] is _not_ the hot shard - hotShard.reserve(1 << 16); - for (size_t i = 0; i < 1 << 16; i++) - hotShard.push_back(AttributePair(0, false, 0)); + // The "hot" shard has a capacity of 64K, the others are unbounded. + pairs.push_back(DequeMap(1 << 16)); + // Reserve offset 0 as a sentinel + pairs[0].add(AttributePair(0, false, 0)); + for (size_t i = 1; i < ATTRIBUTE_SHARDS; i++) + pairs.push_back(DequeMap()); } void finalize() { finalized = true; } @@ -190,23 +202,7 @@ class AttributePairStore { const AttributePair& getPairUnsafe(uint32_t i) const; uint32_t addPair(AttributePair& pair, bool isHot); - struct key_value_less_ptr { - bool operator()(AttributePair const* lhs, AttributePair const* rhs) const { - if (lhs->minzoom != rhs->minzoom) - return lhs->minzoom < rhs->minzoom; - if (lhs->keyIndex != rhs->keyIndex) - return lhs->keyIndex < rhs->keyIndex; - if (lhs->valueType != rhs->valueType) return lhs->valueType < rhs->valueType; - - if (lhs->hasStringValue()) return lhs->pooledString() < rhs->pooledString(); - if (lhs->hasBoolValue()) return lhs->boolValue() < rhs->boolValue(); - if (lhs->hasFloatValue()) return lhs->floatValue() < rhs->floatValue(); - throw std::runtime_error("Invalid type in attribute store"); - } - }; - - std::vector> pairs; - std::vector> pairsMaps; + std::vector> pairs; private: bool finalized; @@ -218,41 +214,37 @@ class AttributePairStore { // we suspect will be popular. It only ever has 64KB items, // so that we can reference it with a short. mutable std::vector pairsMutex; - std::atomic hotShardSize; - std::vector hotShard; }; // AttributeSet is a set of AttributePairs // = the complete attributes for one object struct AttributeSet { - struct less_ptr { - bool operator()(const AttributeSet* lhs, const AttributeSet* rhs) const { - if (lhs->useVector != rhs->useVector) - return lhs->useVector < rhs->useVector; - - if (lhs->useVector) { - if (lhs->intValues.size() != rhs->intValues.size()) - return lhs->intValues.size() < rhs->intValues.size(); - - for (int i = 0; i < lhs->intValues.size(); i++) { - if (lhs->intValues[i] != rhs->intValues[i]) { - return lhs->intValues[i] < rhs->intValues[i]; - } - } + bool operator<(const AttributeSet& other) const { + if (useVector != other.useVector) + return useVector < other.useVector; - return false; - } + if (useVector) { + if (intValues.size() != other.intValues.size()) + return intValues.size() < other.intValues.size(); - for (int i = 0; i < sizeof(lhs->shortValues)/sizeof(lhs->shortValues[0]); i++) { - if (lhs->shortValues[i] != rhs->shortValues[i]) { - return lhs->shortValues[i] < rhs->shortValues[i]; + for (int i = 0; i < intValues.size(); i++) { + if (intValues[i] != other.intValues[i]) { + return intValues[i] < other.intValues[i]; } } return false; } - }; + + for (int i = 0; i < sizeof(shortValues)/sizeof(shortValues[0]); i++) { + if (shortValues[i] != other.shortValues[i]) { + return shortValues[i] < other.shortValues[i]; + } + } + + return false; + } size_t hash() const { // Values are in canonical form after finalizeSet is called, so @@ -273,6 +265,7 @@ struct AttributeSet { return idx; } + bool operator!=(const AttributeSet& other) const { return !(*this == other); } bool operator==(const AttributeSet &other) const { // Equivalent if, for every value in values, there is a value in other.values // whose pair is the same. @@ -412,7 +405,6 @@ struct AttributeStore { AttributeStore(): finalized(false), sets(ATTRIBUTE_SHARDS), - setsMaps(ATTRIBUTE_SHARDS), setsMutex(ATTRIBUTE_SHARDS), lookups(0) { } @@ -422,8 +414,7 @@ struct AttributeStore { private: bool finalized; - std::vector> sets; - std::vector> setsMaps; + std::vector> sets; mutable std::vector setsMutex; mutable std::mutex mutex; diff --git a/include/deque_map.h b/include/deque_map.h new file mode 100644 index 00000000..2ec20387 --- /dev/null +++ b/include/deque_map.h @@ -0,0 +1,128 @@ +#ifndef DEQUE_MAP_H +#define DEQUE_MAP_H + +#include +#include +#include +#include +#include + +// A class which looks deep within the soul of some instance of +// a class T and assigns it a number based on the order in which +// it joined (or reminds it of its number). +// +// Used to translate an 8-byte pointer into a 4-byte ID that can be +// used repeatedly. +template +class DequeMap { +public: + DequeMap(): maxSize(0) {} + DequeMap(uint32_t maxSize): maxSize(maxSize) {} + + bool full() const { + return maxSize != 0 && size() == maxSize; + } + + // If `entry` is already in the map, return its index. + // Otherwise, if maxSize is `0`, or greater than the number of entries in the map, + // add the item and return its index. + // Otherwise, return -1. + int32_t add(const T& entry) { + // Search to see if we've already got this entry. + const auto offsets = boost::irange(0, keys.size()); + const auto it = std::lower_bound( + offsets.begin(), + offsets.end(), + entry, + [&](const auto &e, auto id) { + return objects.at(keys[e]) < id; + } + ); + + // We do, return its index. + if (it != offsets.end() && objects[keys[*it]] == entry) + return keys[*it]; + + if (maxSize > 0 && objects.size() >= maxSize) + return -1; + + // We don't, so store it... + const uint32_t newIndex = objects.size(); + objects.push_back(entry); + + // ...and add its index to our keys vector. + const uint32_t keysOffset = it == offsets.end() ? offsets.size() : *it; + + const uint32_t desiredSize = keys.size() + 1; + + // Amortize growth + if (keys.capacity() < desiredSize) + keys.reserve(keys.capacity() * 1.5); + + keys.resize(desiredSize); + + // Unless we're adding to the end, we need to shuffle existing keys down + // to make room for our new index. + if (keysOffset != newIndex) { + std::memmove(&keys[keysOffset + 1], &keys[keysOffset], sizeof(uint32_t) * (keys.size() - 1 - keysOffset)); + } + + keys[keysOffset] = newIndex; + return newIndex; + } + + void clear() { + objects.clear(); + keys.clear(); + } + + // Returns the index of `entry` if present, -1 otherwise. + int32_t find(const T& entry) const { + const auto offsets = boost::irange(0, keys.size()); + const auto it = std::lower_bound( + offsets.begin(), + offsets.end(), + entry, + [&](const auto &e, auto id) { + return objects.at(keys[e]) < id; + } + ); + + // We do, return its index. + if (it != offsets.end() && objects[keys[*it]] == entry) + return keys[*it]; + + return -1; + } + + const T& at(uint32_t index) const { + return objects.at(index); + } + + size_t size() const { return objects.size(); } + + struct iterator { + const DequeMap& dm; + int offset; + iterator(const DequeMap& dm, int offset): dm(dm), offset(offset) {} + void operator++() { offset++; } + bool operator!=(iterator& other) { return offset != other.offset; } + const T& operator*() const { return dm.objects[dm.keys[offset]]; } + }; + + iterator begin() const { return iterator{*this, 0}; } + iterator end() const { return iterator{*this, keys.size()}; } + +private: + uint32_t maxSize; + + // Using a deque is necessary, as it provides pointer-stability for previously + // added objects when it grows the storage (as opposed to, e.g., vector). + std::deque objects; + + // Whereas `objects` is ordered by insertion-time, `keys` is sorted such that + // objects[key[0]] < objects[key[1]] < ... < objects[key[$]] + // operator< of T. + std::vector keys; +}; +#endif diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index 71c0925b..6fbacbe9 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -66,14 +66,22 @@ void AttributePair::ensureStringIsOwned() { } // AttributePairStore -thread_local boost::container::flat_map tlsHotShardMap; -thread_local uint16_t tlsHotShardSize = 0; +thread_local DequeMap tlsHotShard(1 << 16); const AttributePair& AttributePairStore::getPair(uint32_t i) const { uint32_t shard = i >> (32 - SHARD_BITS); uint32_t offset = i & (~(~0u << (32 - SHARD_BITS))); - if (shard == 0) - return hotShard[offset]; + if (shard == 0) { + if (offset < tlsHotShard.size()) + return tlsHotShard.at(offset); + + { + std::lock_guard lock(pairsMutex[0]); + tlsHotShard = pairs[0]; + } + + return tlsHotShard.at(offset); + } std::lock_guard lock(pairsMutex[shard]); return pairs[shard].at(offset); @@ -86,9 +94,6 @@ const AttributePair& AttributePairStore::getPairUnsafe(uint32_t i) const { uint32_t shard = i >> (32 - SHARD_BITS); uint32_t offset = i & (~(~0u << (32 - SHARD_BITS))); - if (shard == 0) - return hotShard[offset]; - return pairs[shard].at(offset); }; @@ -96,35 +101,29 @@ uint32_t AttributePairStore::addPair(AttributePair& pair, bool isHot) { if (isHot) { { // First, check our thread-local map. - const auto& it = tlsHotShardMap.find(&pair); - if (it != tlsHotShardMap.end()) - return it->second; + const auto& index = tlsHotShard.find(pair); + if (index != -1) + return index; } + // Not found, ensure our local map is up-to-date for future calls, // and fall through to the main map. - // - // Note that we can read `hotShard` without a lock, its size is fixed - while (tlsHotShardSize < hotShardSize.load()) { - tlsHotShardSize++; - tlsHotShardMap[&hotShard[tlsHotShardSize]] = tlsHotShardSize; + if (!tlsHotShard.full()) { + std::lock_guard lock(pairsMutex[0]); + tlsHotShard = pairs[0]; } // This might be a popular pair, worth re-using. // Have we already assigned it a hot ID? std::lock_guard lock(pairsMutex[0]); - const auto& it = pairsMaps[0].find(&pair); - if (it != pairsMaps[0].end()) - return it->second; - - if (hotShardSize.load() < 1 << 16) { - hotShardSize++; - uint32_t offset = hotShardSize.load(); + const auto& index = pairs[0].find(pair); + if (index != -1) + return index; + if (!pairs[0].full()) { pair.ensureStringIsOwned(); - hotShard[offset] = pair; - const AttributePair* ptr = &hotShard[offset]; + uint32_t offset = pairs[0].add(pair); uint32_t rv = (0 << (32 - SHARD_BITS)) + offset; - pairsMaps[0][ptr] = rv; return rv; } } @@ -141,21 +140,17 @@ uint32_t AttributePairStore::addPair(AttributePair& pair, bool isHot) { if (shard == 0) shard = 1; std::lock_guard lock(pairsMutex[shard]); - const auto& it = pairsMaps[shard].find(&pair); - if (it != pairsMaps[shard].end()) - return it->second; + const auto& index = pairs[shard].find(pair); + if (index != -1) + return (shard << (32 - SHARD_BITS)) + index; - uint32_t offset = pairs[shard].size(); + pair.ensureStringIsOwned(); + uint32_t offset = pairs[shard].add(pair); if (offset >= (1 << (32 - SHARD_BITS))) throw std::out_of_range("pair shard overflow"); - pair.ensureStringIsOwned(); - pairs[shard].push_back(pair); - const AttributePair* ptr = &pairs[shard][offset]; uint32_t rv = (shard << (32 - SHARD_BITS)) + offset; - - pairsMaps[shard][ptr] = rv; return rv; }; @@ -282,19 +277,11 @@ AttributeIndex AttributeStore::add(AttributeSet &attributes) { std::lock_guard lock(setsMutex[shard]); lookups++; - // Do we already have it? - const auto& existing = setsMaps[shard].find(&attributes); - if (existing != setsMaps[shard].end()) return existing->second; - - // No, so add and return the index - uint32_t offset = sets[shard].size(); + const uint32_t offset = sets[shard].add(attributes); if (offset >= (1 << (32 - SHARD_BITS))) throw std::out_of_range("set shard overflow"); - sets[shard].push_back(attributes); - const AttributeSet* ptr = &sets[shard][offset]; uint32_t rv = (shard << (32 - SHARD_BITS)) + offset; - setsMaps[shard][ptr] = rv; return rv; } @@ -335,7 +322,7 @@ void AttributeStore::reportSize() const { // Print detailed histogram of frequencies of attributes. if (false) { for (int i = 0; i < ATTRIBUTE_SHARDS; i++) { - std::cout << "pairsMaps[" << i << "] has " << pairStore.pairsMaps[i].size() << " entries" << std::endl; + std::cout << "pairs[" << i << "] has " << pairStore.pairs[i].size() << " entries" << std::endl; } std::map tagCountDist; @@ -391,8 +378,8 @@ void AttributeStore::reset() { // This is only used for tests. tlsKeys2Index.clear(); tlsKeys2IndexSize = 0; - tlsHotShardMap.clear(); - tlsHotShardSize = 0; + + tlsHotShard.clear(); } void AttributeStore::finalize() { diff --git a/test/attribute_store.test.cpp b/test/attribute_store.test.cpp index 3f2e28e5..db104a14 100644 --- a/test/attribute_store.test.cpp +++ b/test/attribute_store.test.cpp @@ -22,7 +22,6 @@ MU_TEST(test_attribute_store) { const auto s1Pairs = store.getUnsafe(s1Index); mu_check(s1Pairs.size() == 5); - const auto str1 = std::find_if(s1Pairs.begin(), s1Pairs.end(), [&store](auto ap) { return ap->keyIndex == store.keyStore.key2index("str1"); }); diff --git a/test/deque_map.test.cpp b/test/deque_map.test.cpp new file mode 100644 index 00000000..23a3d3cc --- /dev/null +++ b/test/deque_map.test.cpp @@ -0,0 +1,63 @@ +#include +#include +#include "external/minunit.h" +#include "deque_map.h" + +MU_TEST(test_deque_map) { + DequeMap strs; + + mu_check(strs.size() == 0); + mu_check(!strs.full()); + mu_check(strs.find("foo") == -1); + mu_check(strs.add("foo") == 0); + mu_check(!strs.full()); + mu_check(strs.find("foo") == 0); + mu_check(strs.size() == 1); + mu_check(strs.add("foo") == 0); + mu_check(strs.size() == 1); + mu_check(strs.add("bar") == 1); + mu_check(strs.size() == 2); + mu_check(strs.add("aardvark") == 2); + mu_check(strs.size() == 3); + mu_check(strs.add("foo") == 0); + mu_check(strs.add("bar") == 1); + mu_check(strs.add("quux") == 3); + mu_check(strs.size() == 4); + + mu_check(strs.at(0) == "foo"); + mu_check(strs.at(1) == "bar"); + mu_check(strs.at(2) == "aardvark"); + mu_check(strs.at(3) == "quux"); + + std::vector rv; + for (std::string x : strs) { + rv.push_back(x); + } + mu_check(rv[0] == "aardvark"); + mu_check(rv[1] == "bar"); + mu_check(rv[2] == "foo"); + mu_check(rv[3] == "quux"); + + DequeMap boundedMap(1); + mu_check(!boundedMap.full()); + mu_check(boundedMap.add("foo") == 0); + mu_check(boundedMap.add("foo") == 0); + mu_check(boundedMap.full()); + mu_check(boundedMap.add("bar") == -1); + boundedMap.clear(); + mu_check(!boundedMap.full()); + mu_check(boundedMap.find("foo") == -1); + mu_check(boundedMap.add("bar") == 0); + mu_check(boundedMap.add("bar") == 0); + mu_check(boundedMap.full()); +} + +MU_TEST_SUITE(test_suite_deque_map) { + MU_RUN_TEST(test_deque_map); +} + +int main() { + MU_RUN_SUITE(test_suite_deque_map); + MU_REPORT(); + return MU_EXIT_CODE; +} From f22cfdfe61e191d1b5185c8018fea74b7b5764cd Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 23 Dec 2023 17:54:58 -0500 Subject: [PATCH 51/81] capture s(this) Seems to save ~1.5 seconds on GB --- src/sorted_node_store.cpp | 82 ++++++++++++++++++++------------------- src/sorted_way_store.cpp | 53 +++++++++++++------------ 2 files changed, 71 insertions(+), 64 deletions(-) diff --git a/src/sorted_node_store.cpp b/src/sorted_node_store.cpp index 174664c3..82dccb55 100644 --- a/src/sorted_node_store.cpp +++ b/src/sorted_node_store.cpp @@ -173,22 +173,23 @@ LatpLon SortedNodeStore::at(const NodeID id) const { // Really naive caching strategy - just cache the last-used chunk. // Probably good enough? - if (s(this).cachedChunk != neededChunk) { - s(this).cachedChunk = neededChunk; - s(this).cacheChunkLons.reserve(256); - s(this).cacheChunkLatps.reserve(256); + ThreadStorage& tls = s(this); + if (tls.cachedChunk != neededChunk) { + tls.cachedChunk = neededChunk; + tls.cacheChunkLons.reserve(256); + tls.cacheChunkLatps.reserve(256); uint8_t* latpData = ptr->data; uint8_t* lonData = ptr->data + latpSize; uint32_t recovdata[256] = {0}; streamvbyte_decode(latpData, recovdata, n); - s(this).cacheChunkLatps[0] = ptr->firstLatp; - zigzag_delta_decode(recovdata, &s(this).cacheChunkLatps[1], n, s(this).cacheChunkLatps[0]); + tls.cacheChunkLatps[0] = ptr->firstLatp; + zigzag_delta_decode(recovdata, &tls.cacheChunkLatps[1], n, tls.cacheChunkLatps[0]); streamvbyte_decode(lonData, recovdata, n); - s(this).cacheChunkLons[0] = ptr->firstLon; - zigzag_delta_decode(recovdata, &s(this).cacheChunkLons[1], n, s(this).cacheChunkLons[0]); + tls.cacheChunkLons[0] = ptr->firstLon; + zigzag_delta_decode(recovdata, &tls.cacheChunkLons[1], n, tls.cacheChunkLons[0]); } size_t nodeOffset = 0; @@ -199,7 +200,7 @@ LatpLon SortedNodeStore::at(const NodeID id) const { if (!(ptr->nodeMask[nodeMaskByte] & (1 << nodeMaskBit))) throw std::out_of_range("SortedNodeStore: node " + std::to_string(id) + " missing, no node"); - return { s(this).cacheChunkLatps[nodeOffset], s(this).cacheChunkLons[nodeOffset] }; + return { tls.cacheChunkLatps[nodeOffset], tls.cacheChunkLons[nodeOffset] }; } UncompressedChunkInfo* ptr = (UncompressedChunkInfo*)basePtr; @@ -241,58 +242,60 @@ size_t SortedNodeStore::size() const { } void SortedNodeStore::insert(const std::vector& elements) { - if (s(this).localNodes == nullptr) { + ThreadStorage& tls = s(this); + if (tls.localNodes == nullptr) { std::lock_guard lock(orphanageMutex); if (workerBuffers.size() == 0) workerBuffers.reserve(256); else if (workerBuffers.size() == workerBuffers.capacity()) throw std::runtime_error("SortedNodeStore doesn't support more than 256 cores"); workerBuffers.push_back(std::vector()); - s(this).localNodes = &workerBuffers.back(); + tls.localNodes = &workerBuffers.back(); } - if (s(this).groupStart == -1) { + if (tls.groupStart == -1) { // Mark where the first full group starts, so we know when to transition // out of collecting orphans. - s(this).groupStart = elements[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + tls.groupStart = elements[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } int i = 0; - while (s(this).collectingOrphans && i < elements.size()) { + while (tls.collectingOrphans && i < elements.size()) { const element_t& el = elements[i]; - if (el.first >= s(this).groupStart + (GroupSize * ChunkSize)) { - s(this).collectingOrphans = false; + if (el.first >= tls.groupStart + (GroupSize * ChunkSize)) { + tls.collectingOrphans = false; // Calculate new groupStart, rounding to previous boundary. - s(this).groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); - collectOrphans(*s(this).localNodes); - s(this).localNodes->clear(); + tls.groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + collectOrphans(*tls.localNodes); + tls.localNodes->clear(); } - s(this).localNodes->push_back(el); + tls.localNodes->push_back(el); i++; } while(i < elements.size()) { const element_t& el = elements[i]; - if (el.first >= s(this).groupStart + (GroupSize * ChunkSize)) { - publishGroup(*s(this).localNodes); - s(this).localNodes->clear(); - s(this).groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + if (el.first >= tls.groupStart + (GroupSize * ChunkSize)) { + publishGroup(*tls.localNodes); + tls.localNodes->clear(); + tls.groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } - s(this).localNodes->push_back(el); + tls.localNodes->push_back(el); i++; } } void SortedNodeStore::batchStart() { - s(this).collectingOrphans = true; - s(this).groupStart = -1; - if (s(this).localNodes == nullptr || s(this).localNodes->size() == 0) + ThreadStorage& tls = s(this); + tls.collectingOrphans = true; + tls.groupStart = -1; + if (tls.localNodes == nullptr || tls.localNodes->size() == 0) return; - collectOrphans(*s(this).localNodes); - s(this).localNodes->clear(); + collectOrphans(*tls.localNodes); + tls.localNodes->clear(); } void SortedNodeStore::finalize(size_t threadNum) { @@ -467,22 +470,23 @@ void SortedNodeStore::publishGroup(const std::vector& nodes) { GroupInfo* groupInfo = nullptr; - if (s(this).arenaSpace < groupSpace) { + ThreadStorage& tls = s(this); + if (tls.arenaSpace < groupSpace) { // A full group takes ~330KB. Nodes are read _fast_, and there ends // up being contention calling the allocator when reading the // planet on a machine with 48 cores -- so allocate in large chunks. - s(this).arenaSpace = 4 * 1024 * 1024; - totalAllocatedSpace += s(this).arenaSpace; - s(this).arenaPtr = (char*)void_mmap_allocator::allocate(s(this).arenaSpace); - if (s(this).arenaPtr == nullptr) + tls.arenaSpace = 4 * 1024 * 1024; + totalAllocatedSpace += tls.arenaSpace; + tls.arenaPtr = (char*)void_mmap_allocator::allocate(tls.arenaSpace); + if (tls.arenaPtr == nullptr) throw std::runtime_error("SortedNodeStore: failed to allocate arena"); std::lock_guard lock(orphanageMutex); - allocatedMemory.push_back(std::make_pair((void*)s(this).arenaPtr, s(this).arenaSpace)); + allocatedMemory.push_back(std::make_pair((void*)tls.arenaPtr, tls.arenaSpace)); } - s(this).arenaSpace -= groupSpace; - groupInfo = (GroupInfo*)s(this).arenaPtr; - s(this).arenaPtr += groupSpace; + tls.arenaSpace -= groupSpace; + groupInfo = (GroupInfo*)tls.arenaPtr; + tls.arenaPtr += groupSpace; if (groups[groupIndex] != nullptr) throw std::runtime_error("SortedNodeStore: group already present"); diff --git a/src/sorted_way_store.cpp b/src/sorted_way_store.cpp index 27ae6ae2..450a4bcc 100644 --- a/src/sorted_way_store.cpp +++ b/src/sorted_way_store.cpp @@ -32,7 +32,7 @@ namespace SortedWayStoreTypes { thread_local std::deque> threadStorage; - ThreadStorage& s(const SortedWayStore* who) { + inline ThreadStorage& s(const SortedWayStore* who) { for (auto& entry : threadStorage) if (entry.first == who) return entry.second; @@ -214,46 +214,47 @@ void SortedWayStore::insertNodes(const std::vector lock(orphanageMutex); if (workerBuffers.size() == 0) workerBuffers.reserve(256); else if (workerBuffers.size() == workerBuffers.capacity()) throw std::runtime_error("SortedWayStore doesn't support more than 256 cores"); workerBuffers.push_back(std::vector>>()); - s(this).localWays = &workerBuffers.back(); + tls.localWays = &workerBuffers.back(); } - if (s(this).groupStart == -1) { + if (tls.groupStart == -1) { // Mark where the first full group starts, so we know when to transition // out of collecting orphans. - s(this).groupStart = newWays[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + tls.groupStart = newWays[0].first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } int i = 0; - while (s(this).collectingOrphans && i < newWays.size()) { + while (tls.collectingOrphans && i < newWays.size()) { const auto& el = newWays[i]; - if (el.first >= s(this).groupStart + (GroupSize * ChunkSize)) { - s(this).collectingOrphans = false; + if (el.first >= tls.groupStart + (GroupSize * ChunkSize)) { + tls.collectingOrphans = false; // Calculate new groupStart, rounding to previous boundary. - s(this).groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); - collectOrphans(*s(this).localWays); - s(this).localWays->clear(); + tls.groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + collectOrphans(*tls.localWays); + tls.localWays->clear(); } - s(this).localWays->push_back(el); + tls.localWays->push_back(el); i++; } while(i < newWays.size()) { const auto& el = newWays[i]; - if (el.first >= s(this).groupStart + (GroupSize * ChunkSize)) { - publishGroup(*s(this).localWays); - s(this).localWays->clear(); - s(this).groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); + if (el.first >= tls.groupStart + (GroupSize * ChunkSize)) { + publishGroup(*tls.localWays); + tls.localWays->clear(); + tls.groupStart = el.first / (GroupSize * ChunkSize) * (GroupSize * ChunkSize); } - s(this).localWays->push_back(el); + tls.localWays->push_back(el); i++; } } @@ -297,13 +298,14 @@ void SortedWayStore::finalize(unsigned int threadNum) { } void SortedWayStore::batchStart() { - s(this).collectingOrphans = true; - s(this).groupStart = -1; - if (s(this).localWays == nullptr || s(this).localWays->size() == 0) + ThreadStorage& tls = s(this); + tls.collectingOrphans = true; + tls.groupStart = -1; + if (tls.localWays == nullptr || tls.localWays->size() == 0) return; - collectOrphans(*s(this).localWays); - s(this).localWays->clear(); + collectOrphans(*tls.localWays); + tls.localWays->clear(); } void SortedWayStore::collectOrphans(const std::vector>>& orphans) { @@ -476,6 +478,7 @@ void populateMask(uint8_t* mask, const std::vector& ids) { } void SortedWayStore::publishGroup(const std::vector>>& ways) { + ThreadStorage& tls = s(this); totalWays += ways.size(); if (ways.size() == 0) { throw std::runtime_error("SortedWayStore: group is empty"); @@ -519,12 +522,12 @@ void SortedWayStore::publishGroup(const std::vectorwayIds.push_back(id % ChunkSize); - uint16_t flags = encodeWay(way.second, s(this).encodedWay, compressWays && way.second.size() >= 4); + uint16_t flags = encodeWay(way.second, tls.encodedWay, compressWays && way.second.size() >= 4); lastChunk->wayFlags.push_back(flags); std::vector encoded; - encoded.resize(s(this).encodedWay.size()); - memcpy(encoded.data(), s(this).encodedWay.data(), s(this).encodedWay.size()); + encoded.resize(tls.encodedWay.size()); + memcpy(encoded.data(), tls.encodedWay.data(), tls.encodedWay.size()); lastChunk->encodedWays.push_back(std::move(encoded)); } From efd66bbfe82343621f314dcf0f39b6fb0dbb8d36 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 23 Dec 2023 18:02:48 -0500 Subject: [PATCH 52/81] fix warning --- include/deque_map.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/deque_map.h b/include/deque_map.h index 2ec20387..283f8490 100644 --- a/include/deque_map.h +++ b/include/deque_map.h @@ -103,7 +103,7 @@ class DequeMap { struct iterator { const DequeMap& dm; - int offset; + size_t offset; iterator(const DequeMap& dm, int offset): dm(dm), offset(offset) {} void operator++() { offset++; } bool operator!=(iterator& other) { return offset != other.offset; } From db89f8bd3ceaca67c7c7e6cf8b84b63d82e49063 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 23 Dec 2023 18:13:46 -0500 Subject: [PATCH 53/81] fix warning, really --- include/deque_map.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/deque_map.h b/include/deque_map.h index 283f8490..bcb4ddbc 100644 --- a/include/deque_map.h +++ b/include/deque_map.h @@ -104,7 +104,7 @@ class DequeMap { struct iterator { const DequeMap& dm; size_t offset; - iterator(const DequeMap& dm, int offset): dm(dm), offset(offset) {} + iterator(const DequeMap& dm, size_t offset): dm(dm), offset(offset) {} void operator++() { offset++; } bool operator!=(iterator& other) { return offset != other.offset; } const T& operator*() const { return dm.objects[dm.keys[offset]]; } From 60e5261bd15bef0a3eecf648e9219712e5508070 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 23 Dec 2023 18:17:48 -0500 Subject: [PATCH 54/81] fewer shards Shard 1 (North America) is ~4.8GB of nodes, shard 4 (some of Europe) is 3.7GB. Even ignoring the memory savings in the recent commits, these could be merged. --- src/sharded_node_store.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sharded_node_store.cpp b/src/sharded_node_store.cpp index 964d61fa..e9a6dc16 100644 --- a/src/sharded_node_store.cpp +++ b/src/sharded_node_store.cpp @@ -61,12 +61,12 @@ size_t pickStore(const LatpLon& el) { const size_t z3x = z4x / 2; const size_t z3y = z4y / 2; - if (z3x == 5 && z3y == 2) return 6; // Western Russia - if (z3x == 4 && z3y == 3) return 6; // North Africa - if (z3x == 5 && z3y == 3) return 6; // India + if (z3x == 5 && z3y == 2) return 5; // Western Russia + if (z3x == 4 && z3y == 3) return 5; // North Africa + if (z3x == 5 && z3y == 3) return 5; // India - if ((z5x == 16 && z5y == 10) || (z5x == 16 && z5y == 11)) return 5; // some of Central Europe - if ((z5x == 17 && z5y == 10) || (z5x == 17 && z5y == 11)) return 4; // some more of Central Europe + if ((z5x == 16 && z5y == 10) || (z5x == 16 && z5y == 11)) return 4; // some of Central Europe + if ((z5x == 17 && z5y == 10) || (z5x == 17 && z5y == 11)) return 1; // some more of Central Europe if (z3x == 4 && z3y == 2) return 3; // rest of Central Europe From 6886e6b5201baa917cc8f287a50d1ac13bd12792 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 23 Dec 2023 22:22:21 -0500 Subject: [PATCH 55/81] fix includes --- src/helpers.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/helpers.cpp b/src/helpers.cpp index bc862e70..df210b95 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "helpers.h" From f95e13d0153956953e13c96542fddbc1beece7e1 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sat, 23 Dec 2023 22:47:08 -0500 Subject: [PATCH 56/81] rm osmformat.proto, pbf_blocks.{h,cpp} --- CMakeLists.txt | 7 +- Makefile | 2 - include/osmformat.proto | 226 --------------------------------------- include/output_object.h | 1 - include/pbf_blocks.h | 48 --------- include/read_pbf.h | 1 - include/write_geometry.h | 1 - src/pbf_blocks.cpp | 122 --------------------- src/read_pbf.cpp | 1 - 9 files changed, 1 insertion(+), 408 deletions(-) delete mode 100644 include/osmformat.proto delete mode 100644 include/pbf_blocks.h delete mode 100644 src/pbf_blocks.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bd49cdce..ba6280e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,10 +81,6 @@ ADD_CUSTOM_COMMAND(OUTPUT vector_tile.pb.cc vector_tile.pb.h COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} ARGS --cpp_out ${CMAKE_BINARY_DIR} -I ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/include/vector_tile.proto) -ADD_CUSTOM_COMMAND(OUTPUT osmformat.pb.cc osmformat.pb.h - COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} - ARGS --cpp_out ${CMAKE_BINARY_DIR} -I ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/include/osmformat.proto) - file(GLOB tilemaker_src_files src/attribute_store.cpp src/coordinates.cpp @@ -101,7 +97,6 @@ file(GLOB tilemaker_src_files src/osm_mem_tiles.cpp src/osm_store.cpp src/output_object.cpp - src/pbf_blocks.cpp src/pbf_reader.cpp src/pmtiles.cpp src/pooled_string.cpp @@ -119,7 +114,7 @@ file(GLOB tilemaker_src_files src/way_stores.cpp src/write_geometry.cpp ) -add_executable(tilemaker vector_tile.pb.cc osmformat.pb.cc ${tilemaker_src_files}) +add_executable(tilemaker vector_tile.pb.cc ${tilemaker_src_files}) target_include_directories(tilemaker PRIVATE include) target_include_directories(tilemaker PRIVATE ${CMAKE_BINARY_DIR}) # for generated files target_link_libraries(tilemaker diff --git a/Makefile b/Makefile index 44fe0388..6d618783 100644 --- a/Makefile +++ b/Makefile @@ -93,7 +93,6 @@ INC := -I$(PLATFORM_PATH)/include -isystem ./include -I./src $(LUA_CFLAGS) all: tilemaker tilemaker: \ - include/osmformat.pb.o \ include/vector_tile.pb.o \ src/attribute_store.o \ src/coordinates_geom.o \ @@ -110,7 +109,6 @@ tilemaker: \ src/osm_mem_tiles.o \ src/osm_store.o \ src/output_object.o \ - src/pbf_blocks.o \ src/pmtiles.o \ src/pooled_string.o \ src/pbf_reader.o \ diff --git a/include/osmformat.proto b/include/osmformat.proto deleted file mode 100644 index 93060586..00000000 --- a/include/osmformat.proto +++ /dev/null @@ -1,226 +0,0 @@ -syntax = "proto2"; - -option java_package = "crosby.binary"; - -/* OSM Binary file format - -This is the master schema file of the OSM binary file format. This -file is designed to support limited random-access and future -extendability. - -A binary OSM file consists of a sequence of FileBlocks (please see -fileformat.proto). The first fileblock contains a serialized instance -of HeaderBlock, followed by a sequence of PrimitiveBlock blocks that -contain the primitives. - -Each primitiveblock is designed to be independently parsable. It -contains a string table storing all strings in that block (keys and -values in tags, roles in relations, usernames, etc.) as well as -metadata containing the precision of coordinates or timestamps in that -block. - -A primitiveblock contains a sequence of primitive groups, each -containing primitives of the same type (nodes, densenodes, ways, -relations). Coordinates are stored in signed 64-bit integers. Lat&lon -are measured in units nanodegrees. The default of -granularity of 100 nanodegrees corresponds to about 1cm on the ground, -and a full lat or lon fits into 32 bits. - -Converting an integer to a lattitude or longitude uses the formula: -$OUT = IN * granularity / 10**9$. Many encoding schemes use delta -coding when representing nodes and relations. - -*/ - -/* Added */ - -message BlobHeader { - required string type = 1; - optional bytes indexdata = 2; - required int32 datasize = 3; -} -message Blob { - optional bytes raw = 1; // No compression - optional int32 raw_size = 2; // Only set when compressed, to the uncompressed size - optional bytes zlib_data = 3; - // optional bytes lzma_data = 4; // PROPOSED. - // optional bytes OBSOLETE_bzip2_data = 5; // Deprecated. -} - - -////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////// - -/* Contains the file header. */ - -message HeaderBlock { - optional HeaderBBox bbox = 1; - /* Additional tags to aid in parsing this dataset */ - repeated string required_features = 4; - repeated string optional_features = 5; - - optional string writingprogram = 16; - optional string source = 17; // From the bbox field. -} - - -/** The bounding box field in the OSM header. BBOX, as used in the OSM -header. Units are always in nanodegrees -- they do not obey -granularity rules. */ - -message HeaderBBox { - required sint64 left = 1; - required sint64 right = 2; - required sint64 top = 3; - required sint64 bottom = 4; -} - - -/////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////// - - -message PrimitiveBlock { - required StringTable stringtable = 1; - repeated PrimitiveGroup primitivegroup = 2; - - // Granularity, units of nanodegrees, used to store coordinates in this block - optional int32 granularity = 17 [default=100]; - // Offset value between the output coordinates coordinates and the granularity grid in unites of nanodegrees. - optional int64 lat_offset = 19 [default=0]; - optional int64 lon_offset = 20 [default=0]; - -// Granularity of dates, normally represented in units of milliseconds since the 1970 epoch. - optional int32 date_granularity = 18 [default=1000]; - - - // Proposed extension: - //optional BBox bbox = 19; -} - -// Group of OSMPrimitives. All primitives in a group must be the same type. -message PrimitiveGroup { - repeated Node nodes = 1; - optional DenseNodes dense = 2; - repeated Way ways = 3; - repeated Relation relations = 4; - repeated ChangeSet changesets = 5; -} - - -/** String table, contains the common strings in each block. - - Note that we reserve index '0' as a delimiter, so the entry at that - index in the table is ALWAYS blank and unused. - - */ -message StringTable { - repeated bytes s = 1; -} - -/* Optional metadata that may be included into each primitive. */ -message Info { - optional int32 version = 1 [default = -1]; - optional int32 timestamp = 2; - optional int64 changeset = 3; - optional int32 uid = 4; - optional int32 user_sid = 5; // String IDs -} - -/** Optional metadata that may be included into each primitive. Special dense format used in DenseNodes. */ -message DenseInfo { - repeated int32 version = 1 [packed = true]; - repeated sint64 timestamp = 2 [packed = true]; // DELTA coded - repeated sint64 changeset = 3 [packed = true]; // DELTA coded - repeated sint32 uid = 4 [packed = true]; // DELTA coded - repeated sint32 user_sid = 5 [packed = true]; // String IDs for usernames. DELTA coded -} - - -// TODO: REMOVE THIS? NOT in osmosis schema. -message ChangeSet { - required int64 id = 1; - // Parallel arrays. - repeated uint32 keys = 2 [packed = true]; // String IDs. - repeated uint32 vals = 3 [packed = true]; // String IDs. - - optional Info info = 4; - - required int64 created_at = 8; - optional int64 closetime_delta = 9; - required bool open = 10; - optional HeaderBBox bbox = 11; -} - - -message Node { - required sint64 id = 1; - // Parallel arrays. - repeated uint32 keys = 2 [packed = true]; // String IDs. - repeated uint32 vals = 3 [packed = true]; // String IDs. - - optional Info info = 4; // May be omitted in omitmeta - - required sint64 lat = 8; - required sint64 lon = 9; -} - -/* Used to densly represent a sequence of nodes that do not have any tags. - -We represent these nodes columnwise as five columns: ID's, lats, and -lons, all delta coded. When metadata is not omitted, - -We encode keys & vals for all nodes as a single array of integers -containing key-stringid and val-stringid, using a stringid of 0 as a -delimiter between nodes. - - ( ( )* '0' )* - */ - -message DenseNodes { - repeated sint64 id = 1 [packed = true]; // DELTA coded - - //repeated Info info = 4; - optional DenseInfo denseinfo = 5; - - repeated sint64 lat = 8 [packed = true]; // DELTA coded - repeated sint64 lon = 9 [packed = true]; // DELTA coded - - // Special packing of keys and vals into one array. May be empty if all nodes in this block are tagless. - repeated int32 keys_vals = 10 [packed = true]; -} - - -message Way { - required int64 id = 1; - // Parallel arrays. - repeated uint32 keys = 2 [packed = true]; - repeated uint32 vals = 3 [packed = true]; - - optional Info info = 4; - - repeated sint64 refs = 8 [packed = true]; // DELTA coded - repeated sint64 lats = 9 [packed = true]; - repeated sint64 lons = 10 [packed = true]; -} - -message Relation { - enum MemberType { - NODE = 0; - WAY = 1; - RELATION = 2; - } - required int64 id = 1; - - // Parallel arrays. - repeated uint32 keys = 2 [packed = true]; - repeated uint32 vals = 3 [packed = true]; - - optional Info info = 4; - - // Parallel arrays - repeated int32 roles_sid = 8 [packed = true]; - repeated sint64 memids = 9 [packed = true]; // DELTA encoded - repeated MemberType types = 10 [packed = true]; -} - diff --git a/include/output_object.h b/include/output_object.h index 385fd46d..9afd5cba 100644 --- a/include/output_object.h +++ b/include/output_object.h @@ -12,7 +12,6 @@ #include "osm_store.h" // Protobuf -#include "osmformat.pb.h" #include "vector_tile.pb.h" enum OutputGeometryType : unsigned int { POINT_, LINESTRING_, MULTILINESTRING_, POLYGON_ }; diff --git a/include/pbf_blocks.h b/include/pbf_blocks.h deleted file mode 100644 index 5cc28969..00000000 --- a/include/pbf_blocks.h +++ /dev/null @@ -1,48 +0,0 @@ -/*! \file */ -#ifndef _PBF_BLOCKS_H -#define _PBF_BLOCKS_H - -#include -#include -#include -#include - -// Protobuf -#include "osmformat.pb.h" -#include "vector_tile.pb.h" - -/* ------------------- - Protobuf handling - ------------------- */ - -// Read and parse a protobuf message -void readMessage(google::protobuf::Message *message, std::istream &input, unsigned int size); - -// Read an osm.pbf sequence of header length -> BlobHeader -> Blob -// and parse the unzipped contents into a message -BlobHeader readHeader(std::istream &input); -void readBlock(google::protobuf::Message *messagePtr, std::size_t datasize, std::istream &input); - -void writeBlock(google::protobuf::Message *messagePtr, std::ostream &output, std::string headerType); -/* ------------------- - Tag handling - ------------------- */ - -// Populate an array with the contents of a StringTable -void readStringTable(std::vector *strPtr, PrimitiveBlock *pbPtr); - -/// Populate a map with the reverse contents of a StringTable (i.e. string->num) -void readStringMap(std::map *mapPtr, PrimitiveBlock *pbPtr); - -/// Read the tags for a way into a hash -/// requires strings array to have been populated by readStringTable -std::map getTags(std::vector *strPtr, Way *wayPtr); - -/// Find the index of a string in the StringTable, adding it if it's not there -unsigned int findStringInTable(std::string *strPtr, std::map *mapPtr, PrimitiveBlock *pbPtr); - -/// Set a tag for a way to a new value -void setTag(Way *wayPtr, unsigned int keyIndex, unsigned int valueIndex); - -#endif //_PBF_BLOCKS_H - diff --git a/include/read_pbf.h b/include/read_pbf.h index a514d322..65ce9ad7 100644 --- a/include/read_pbf.h +++ b/include/read_pbf.h @@ -12,7 +12,6 @@ #include // Protobuf -#include "osmformat.pb.h" #include "vector_tile.pb.h" class OsmLuaProcessing; diff --git a/include/write_geometry.h b/include/write_geometry.h index 8d1d014b..985b7b66 100644 --- a/include/write_geometry.h +++ b/include/write_geometry.h @@ -9,7 +9,6 @@ #include "coordinates_geom.h" // Protobuf -#include "osmformat.pb.h" #include "vector_tile.pb.h" typedef std::vector > XYString; diff --git a/src/pbf_blocks.cpp b/src/pbf_blocks.cpp deleted file mode 100644 index 32316dab..00000000 --- a/src/pbf_blocks.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "pbf_blocks.h" -#include "helpers.h" -#include -using namespace std; - -/* ------------------- - Protobuf handling - ------------------- */ - -// Read and parse a protobuf message -void readMessage(google::protobuf::Message *message, istream &input, unsigned int size) { - vector buffer(size); - input.read(&buffer.front(), size); - message->ParseFromArray(&buffer.front(), size); -} - -// Read an osm.pbf sequence of header length -> BlobHeader -> Blob -// and parse the unzipped contents into a message -BlobHeader readHeader(istream &input) { - BlobHeader bh; - - unsigned int size; - input.read((char*)&size, sizeof(size)); - if (input.eof()) { return bh; } - endian_swap(size); - - // get BlobHeader and parse - readMessage(&bh, input, size); - return bh; -} - -void readBlock(google::protobuf::Message *messagePtr, std::size_t datasize, istream &input) { - if (input.eof()) { return ; } - - // get Blob and parse - Blob blob; - readMessage(&blob, input, datasize); - - // Unzip the gzipped content - string contents; - decompress_string(contents, blob.zlib_data().data(), blob.zlib_data().size(), false); - messagePtr->ParseFromString(contents); -} - -void writeBlock(google::protobuf::Message *messagePtr, ostream &output, string headerType) { - // encode the message - string serialised; - messagePtr->SerializeToString(&serialised); - // create a blob and store it - Blob blob; - blob.set_raw_size(serialised.length()); - blob.set_zlib_data(compress_string(serialised)); - // encode the blob - string blob_encoded; - blob.SerializeToString(&blob_encoded); - - // create the BlobHeader - BlobHeader bh; - bh.set_type(headerType); - bh.set_datasize(blob_encoded.length()); - // encode it - string header_encoded; - bh.SerializeToString(&header_encoded); - - // write out - unsigned int bhLength=header_encoded.length(); - endian_swap(bhLength); - output.write(reinterpret_cast(&bhLength), 4); - output.write(header_encoded.c_str(), header_encoded.length() ); - output.write(blob_encoded.c_str(), blob_encoded.length() ); -} - -/* ------------------- - Tag handling - ------------------- */ - -// Populate an array with the contents of a StringTable -void readStringTable(vector *strPtr, PrimitiveBlock *pbPtr) { - strPtr->resize(pbPtr->stringtable().s_size()); - for (int i=0; istringtable().s_size(); i++) { - (*strPtr)[i] = pbPtr->stringtable().s(i); // dereference strPtr to get strings - } -} - -// Populate a map with the reverse contents of a StringTable (i.e. string->num) -void readStringMap(map *mapPtr, PrimitiveBlock *pbPtr) { - for (int i=0; istringtable().s_size(); i++) { - mapPtr->insert(pair (pbPtr->stringtable().s(i), i)); - } -} - -// Read the tags for a way into a hash -// requires strings array to have been populated by readStringTable -map getTags(vector *strPtr, Way *wayPtr) { - map tags; - for (int n=0; nkeys_size(); n++) { - tags[(*strPtr)[wayPtr->keys(n)]] = (*strPtr)[wayPtr->vals(n)]; - } - return tags; -} - -// Find the index of a string in the StringTable, adding it if it's not there -unsigned int findStringInTable(string *strPtr, map *mapPtr, PrimitiveBlock *pbPtr) { - if (mapPtr->find(*strPtr) == mapPtr->end()) { - pbPtr->mutable_stringtable()->add_s(*strPtr); - unsigned int ix = pbPtr->stringtable().s_size()-1; - mapPtr->insert(pair (*strPtr, ix)); - } - return mapPtr->at(*strPtr); -} - -// Set a tag for a way to a new value -void setTag(Way *wayPtr, unsigned int keyIndex, unsigned int valueIndex) { - for (int i=0; ikeys_size(); i++) { - if (wayPtr->keys(i)==keyIndex) { - wayPtr->mutable_vals()->Set(i,valueIndex); - return; - } - } - wayPtr->mutable_keys()->Add(keyIndex); - wayPtr->mutable_vals()->Add(valueIndex); -} diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index 3c436190..b58f8705 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -1,6 +1,5 @@ #include #include "read_pbf.h" -#include "pbf_blocks.h" #include "pbf_reader.h" #include From 934ccfe91775631a6987b01e0489ad1b0d9d39d3 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 24 Dec 2023 00:14:14 -0500 Subject: [PATCH 57/81] rejig to remove thread locals --- include/pbf_reader.h | 71 ++++++++++++---- src/pbf_reader.cpp | 173 +++++++++++++++++++-------------------- src/read_pbf.cpp | 19 +++-- test/pbf_reader.test.cpp | 13 +-- 4 files changed, 157 insertions(+), 119 deletions(-) diff --git a/include/pbf_reader.h b/include/pbf_reader.h index d2faf44e..4f308683 100644 --- a/include/pbf_reader.h +++ b/include/pbf_reader.h @@ -106,7 +106,7 @@ namespace PbfReader { enum class PrimitiveGroupType: char { Node = 1, DenseNodes = 2, Way = 3, Relation = 4, ChangeSet = 5}; - struct Nodes { + struct DenseNodes { struct Node { uint64_t id; int32_t lon; @@ -118,14 +118,24 @@ namespace PbfReader { struct Iterator { int32_t offset; Node node; + DenseNodes& nodes; bool operator!=(Iterator& other) const; void operator++(); Node& operator*(); }; + + std::vector ids; + std::vector lons; + std::vector lats; + std::vector tagStart; + std::vector tagEnd; + std::vector keyValues; Iterator begin(); Iterator end(); bool empty(); + void clear(); + void readDenseNodes(protozero::data_view data); }; struct Way { @@ -152,37 +162,50 @@ namespace PbfReader { struct Iterator { protozero::pbf_message message; int offset; + Way& way; bool operator!=(Iterator& other) const; void operator++(); PbfReader::Way& operator*(); + void readWay(protozero::data_view data); }; + PrimitiveGroup* pg; + Way& way; + Iterator begin(); Iterator end(); bool empty(); - PrimitiveGroup* pg; }; struct Relations { struct Iterator { protozero::pbf_message message; int offset; + Relation& relation; bool operator!=(Iterator& other) const; void operator++(); PbfReader::Relation& operator*(); + void readRelation(protozero::data_view data); }; + + PrimitiveGroup* pg; + Relation& relation; + Iterator begin(); Iterator end(); bool empty(); - - PrimitiveGroup* pg; }; struct PrimitiveGroup { - PrimitiveGroup(protozero::data_view data); - Nodes& nodes() const; + PrimitiveGroup( + protozero::data_view data, + DenseNodes& nodes, + Way& way, + Relation& relation + ); + DenseNodes& nodes() const; Ways& ways() const; Relations& relations() const; PrimitiveGroupType type() const; @@ -193,10 +216,11 @@ namespace PbfReader { void ensureData(); protozero::data_view getDataView(); private: + protozero::data_view data; + DenseNodes& denseNodes; mutable Ways internalWays; mutable Relations internalRelations; PrimitiveGroupType internalType; - protozero::data_view data; bool denseNodesInitialized; }; @@ -230,17 +254,28 @@ namespace PbfReader { PrimitiveGroups groupsImpl; }; - BlobHeader readBlobHeader(std::istream& input); - protozero::data_view readBlob(int32_t datasize, std::istream& input); - HeaderBlock readHeaderBlock(protozero::data_view data); - HeaderBBox readHeaderBBox(protozero::data_view data); - PrimitiveBlock& readPrimitiveBlock(protozero::data_view data); - void readDenseNodes(protozero::data_view data); - void readWay(protozero::data_view data, Way& way); - void readRelation(protozero::data_view data, Relation& relation); - void readStringTable(protozero::data_view data, std::vector& stringTable); - - HeaderBlock readHeaderFromFile(std::istream& input); + // This is a little weird: we use a class only to get private storage + // for multiple PBF readers. Due to the way we plumb the input files + // elsewhere in the system, the readers don't own them, and are not + // responsible for closing them. + class PbfReader { + public: + BlobHeader readBlobHeader(std::istream& input); + protozero::data_view readBlob(int32_t datasize, std::istream& input); + HeaderBlock readHeaderBlock(protozero::data_view data); + HeaderBBox readHeaderBBox(protozero::data_view data); + PrimitiveBlock& readPrimitiveBlock(protozero::data_view data); + void readStringTable(protozero::data_view data, std::vector& stringTable); + HeaderBlock readHeaderFromFile(std::istream& input); + + private: + std::string blobStorage; // the blob as stored in the PBF + std::string blobStorage2; // the blob after decompression, if needed + PrimitiveBlock pb; + DenseNodes denseNodes; + Way way; + Relation relation; + }; } #endif diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp index b03dfe45..6ca64087 100644 --- a/src/pbf_reader.cpp +++ b/src/pbf_reader.cpp @@ -9,37 +9,17 @@ using namespace PbfReader; // Where read_pbf.cpp has higher-level routines that populate our structures, // pbf_reader.cpp has low-level tools that interact with the protobuf. // -// WARNING: PbfReader adopts several constraints to optimize for tilemaker's -// use case. -// -// Objects returned from PbfReader can only be used on the same thread. -// The lifetime of an object is until the earlier of: -// - the thread calls a readXyz function at the same or higher level +// The lifetime of an object is only until someone calls a readXyz function at +// the same or higher level. // - e.g. readPrimitiveGroup invalidates the result of a prior readPrimitiveGroup call, // but not the result of a prior readBlob call -// - the thread ends // // This allows us to re-use buffers to minimize heap churn and allocation cost. // // If you want to persist the data beyond that, you must make a copy in memory // that you own. -namespace PbfReader { - thread_local std::string blobStorage; // the blob as stored in the PBF - thread_local std::string blobStorage2; // the blob after decompression, if needed - thread_local PbfReader::PrimitiveBlock pb; - thread_local std::vector denseNodesIds; - thread_local std::vector denseNodesLons; - thread_local std::vector denseNodesLats; - thread_local std::vector denseNodesTagStart; - thread_local std::vector denseNodesTagEnd; - thread_local std::vector denseNodesKeyValues; - thread_local Way way; - thread_local Relation relation; - thread_local Nodes nodesImpl; -} - -BlobHeader PbfReader::readBlobHeader(std::istream& input) { +BlobHeader PbfReader::PbfReader::readBlobHeader(std::istream& input) { // See https://wiki.openstreetmap.org/wiki/PBF_Format#File_format unsigned int size; input.read((char*)&size, sizeof(size)); @@ -84,7 +64,7 @@ BlobHeader PbfReader::readBlobHeader(std::istream& input) { return { type, datasize }; } -protozero::data_view PbfReader::readBlob(int32_t datasize, std::istream& input) { +protozero::data_view PbfReader::PbfReader::readBlob(int32_t datasize, std::istream& input) { blobStorage.resize(datasize); input.read(&blobStorage[0], datasize); if (input.eof()) @@ -118,7 +98,7 @@ protozero::data_view PbfReader::readBlob(int32_t datasize, std::istream& input) return { &blobStorage2[0], blobStorage2.size() }; } -HeaderBBox PbfReader::readHeaderBBox(protozero::data_view data) { +HeaderBBox PbfReader::PbfReader::readHeaderBBox(protozero::data_view data) { HeaderBBox box{0, 0, 0, 0}; protozero::pbf_message message{data}; @@ -144,7 +124,7 @@ HeaderBBox PbfReader::readHeaderBBox(protozero::data_view data) { return box; } -HeaderBlock PbfReader::readHeaderBlock(protozero::data_view data) { +HeaderBlock PbfReader::PbfReader::readHeaderBlock(protozero::data_view data) { HeaderBlock block{false}; protozero::pbf_message message{data}; @@ -169,7 +149,7 @@ HeaderBlock PbfReader::readHeaderBlock(protozero::data_view data) { return block; } -void PbfReader::readStringTable(protozero::data_view data, std::vector& stringTable) { +void PbfReader::PbfReader::readStringTable(protozero::data_view data, std::vector& stringTable) { protozero::pbf_message message{data}; while (message.next()) { switch (message.tag()) { @@ -182,7 +162,7 @@ void PbfReader::readStringTable(protozero::data_view data, std::vector message{data}; uint64_t id = 0; @@ -222,14 +207,14 @@ void PbfReader::readDenseNodes(protozero::data_view data) { auto pi = message.get_packed_sint64(); for (auto i : pi) { id += i; - denseNodesIds.push_back(id); + ids.push_back(id); } break; } case Schema::DenseNodes::repeated_sint64_lat: { auto pi = message.get_packed_sint64(); for (auto i : pi) { lat += i; - denseNodesLats.push_back(lat); + lats.push_back(lat); } break; } @@ -237,14 +222,14 @@ void PbfReader::readDenseNodes(protozero::data_view data) { auto pi = message.get_packed_sint64(); for (auto i : pi) { lon += i; - denseNodesLons.push_back(lon); + lons.push_back(lon); } break; } case Schema::DenseNodes::repeated_int32_keys_vals: { auto pi = message.get_packed_int32(); for (auto kv : pi) { - denseNodesKeyValues.push_back(kv); + keyValues.push_back(kv); } break; } @@ -256,25 +241,35 @@ void PbfReader::readDenseNodes(protozero::data_view data) { } } - for (uint32_t cur = 0, prev = 0; cur < denseNodesKeyValues.size(); cur++) { - if (denseNodesKeyValues[cur] == 0) { - denseNodesTagStart.push_back(prev); - denseNodesTagEnd.push_back(cur); + for (uint32_t cur = 0, prev = 0; cur < keyValues.size(); cur++) { + if (keyValues[cur] == 0) { + tagStart.push_back(prev); + tagEnd.push_back(cur); prev = cur + 1; } } - while(denseNodesTagStart.size() < denseNodesIds.size()) { - denseNodesTagStart.push_back(0); - denseNodesTagEnd.push_back(0); + while(tagStart.size() < ids.size()) { + tagStart.push_back(0); + tagEnd.push_back(0); } } -PbfReader::PrimitiveGroup::PrimitiveGroup(protozero::data_view data): data(data), denseNodesInitialized(false) { +PbfReader::PrimitiveGroup::PrimitiveGroup( + protozero::data_view data, + DenseNodes& denseNodes, + Way& way, + Relation& relation +): + data(data), + denseNodes(denseNodes), + internalWays({this, way}), + internalRelations({this, relation}), + denseNodesInitialized(false) { } int32_t PbfReader::PrimitiveGroup::translateNodeKeyValue(int32_t i) const { - return denseNodesKeyValues.at(i); + return denseNodes.keyValues.at(i); } protozero::data_view PbfReader::PrimitiveGroup::getDataView() { @@ -283,12 +278,7 @@ protozero::data_view PbfReader::PrimitiveGroup::getDataView() { void PbfReader::PrimitiveGroup::ensureData() { // Reset our thread locals. - denseNodesIds.clear(); - denseNodesLons.clear(); - denseNodesLats.clear(); - denseNodesTagStart.clear(); - denseNodesTagEnd.clear(); - denseNodesKeyValues.clear(); + denseNodes.clear(); internalWays.pg = this; internalRelations.pg = this; @@ -300,7 +290,7 @@ void PbfReader::PrimitiveGroup::ensureData() { break; case Schema::PrimitiveGroup::optional_DenseNodes_dense: internalType = PrimitiveGroupType::DenseNodes; - readDenseNodes(message.get_view()); + denseNodes.readDenseNodes(message.get_view()); break; case Schema::PrimitiveGroup::repeated_Way_ways: internalType = PrimitiveGroupType::Way; @@ -317,41 +307,50 @@ void PbfReader::PrimitiveGroup::ensureData() { } } -Nodes& PrimitiveGroup::nodes() const { return nodesImpl; }; +DenseNodes& PrimitiveGroup::nodes() const { return denseNodes; }; PrimitiveBlock::PrimitiveGroups& PrimitiveBlock::groups() { return groupsImpl; }; -bool PbfReader::Nodes::Iterator::operator!=(Iterator& other) const { +void PbfReader::DenseNodes::clear() { + ids.clear(); + lons.clear(); + lats.clear(); + tagStart.clear(); + tagEnd.clear(); + keyValues.clear(); +} + +bool PbfReader::DenseNodes::Iterator::operator!=(Iterator& other) const { return offset != other.offset; } -void PbfReader::Nodes::Iterator::operator++() { +void PbfReader::DenseNodes::Iterator::operator++() { offset++; - if (offset < denseNodesIds.size()) { - node.id = denseNodesIds[offset]; - node.lon = denseNodesLons[offset]; - node.lat = denseNodesLats[offset]; - node.tagStart = denseNodesTagStart[offset]; - node.tagEnd = denseNodesTagEnd[offset]; + if (offset < nodes.ids.size()) { + node.id = nodes.ids[offset]; + node.lon = nodes.lons[offset]; + node.lat = nodes.lats[offset]; + node.tagStart = nodes.tagStart[offset]; + node.tagEnd = nodes.tagEnd[offset]; } } -PbfReader::Nodes::Node& PbfReader::Nodes::Iterator::operator*() { +PbfReader::DenseNodes::Node& PbfReader::DenseNodes::Iterator::operator*() { return node; } -bool Nodes::empty() { - return denseNodesIds.empty(); +bool DenseNodes::empty() { + return ids.empty(); } -PbfReader::Nodes::Iterator Nodes::begin() { - auto it = Iterator {-1}; +PbfReader::DenseNodes::Iterator DenseNodes::begin() { + auto it = Iterator {-1, Node{}, *this}; ++it; return it; } -PbfReader::Nodes::Iterator Nodes::end() { - return Iterator {static_cast(denseNodesIds.size())}; +PbfReader::DenseNodes::Iterator DenseNodes::end() { + return Iterator {static_cast(ids.size()), Node{}, *this}; } bool PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator::operator!=(Iterator& other) const { @@ -368,7 +367,7 @@ PrimitiveGroup& PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator::operator*( return (*groups)[offset]; } PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator PbfReader::PrimitiveBlock::PrimitiveGroups::begin() { - auto it = PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator {-1, *groups }; + auto it = PrimitiveBlock::PrimitiveGroups::Iterator {-1, *groups }; ++it; return it; } @@ -380,7 +379,7 @@ PbfReader::PrimitiveGroupType PbfReader::PrimitiveGroup::type() const { return internalType; } -void PbfReader::readWay(protozero::data_view data, Way& way) { +void PbfReader::Ways::Iterator::readWay(protozero::data_view data) { protozero::pbf_message message{data}; way.id = 0; @@ -448,12 +447,12 @@ void PbfReader::readWay(protozero::data_view data, Way& way) { Ways& PbfReader::PrimitiveGroup::ways() const { return internalWays; } -bool PbfReader::Ways::Iterator::operator!=(PbfReader::Ways::Iterator& other) const { +bool PbfReader::Ways::Iterator::operator!=(Ways::Iterator& other) const { return offset != other.offset; } void PbfReader::Ways::Iterator::operator++() { if (message.next()) { - readWay(message.get_view(), way); + readWay(message.get_view()); offset++; } else { offset = -1; @@ -467,26 +466,26 @@ bool PbfReader::Ways::empty() { } PbfReader::Ways::Iterator PbfReader::Ways::begin() { if (pg->type() != PrimitiveGroupType::Way) - return Ways::Iterator{protozero::pbf_message{nullptr, 0}, 0}; + return Ways::Iterator{protozero::pbf_message{nullptr, 0}, 0, way}; protozero::pbf_message message{pg->getDataView()}; if (message.next()) { protozero::pbf_message message{pg->getDataView()}; - auto it = Ways::Iterator{message, -1}; + auto it = Ways::Iterator{message, -1, way}; ++it; return it; } - return Ways::Iterator{message, -1}; + return Ways::Iterator{message, -1, way}; } PbfReader::Ways::Iterator PbfReader::Ways::end() { if (pg->type() != PrimitiveGroupType::Way) - return Ways::Iterator{protozero::pbf_message{nullptr, 0}, 0}; + return Ways::Iterator{protozero::pbf_message{nullptr, 0}, 0, way}; - return Ways::Iterator{protozero::pbf_message{nullptr, 0}, -1}; + return Ways::Iterator{protozero::pbf_message{nullptr, 0}, -1, way}; } -void PbfReader::readRelation(protozero::data_view data, Relation& relation) { +void PbfReader::Relations::Iterator::readRelation(protozero::data_view data) { protozero::pbf_message message{data}; relation.id = 0; @@ -551,12 +550,12 @@ void PbfReader::readRelation(protozero::data_view data, Relation& relation) { Relations& PbfReader::PrimitiveGroup::relations() const { return internalRelations; } -bool PbfReader::Relations::Iterator::operator!=(PbfReader::Relations::Iterator& other) const { +bool PbfReader::Relations::Iterator::operator!=(Relations::Iterator& other) const { return offset != other.offset; } void PbfReader::Relations::Iterator::operator++() { if (message.next()) { - readRelation(message.get_view(), relation); + readRelation(message.get_view()); offset++; } else { offset = -1; @@ -570,29 +569,29 @@ bool PbfReader::Relations::empty() { } PbfReader::Relations::Iterator PbfReader::Relations::begin() { if (pg->type() != PrimitiveGroupType::Relation) - return Relations::Iterator{protozero::pbf_message{nullptr, 0}, 0}; + return Relations::Iterator{protozero::pbf_message{nullptr, 0}, 0, relation}; protozero::pbf_message message{pg->getDataView()}; if (message.next()) { protozero::pbf_message message{pg->getDataView()}; - auto it = Relations::Iterator{message, -1}; + auto it = Relations::Iterator{message, -1, relation}; ++it; return it; } - return Relations::Iterator{message, -1}; + return Relations::Iterator{message, -1, relation}; } PbfReader::Relations::Iterator PbfReader::Relations::end() { if (pg->type() != PrimitiveGroupType::Relation) - return Relations::Iterator{protozero::pbf_message{nullptr, 0}, 0}; + return Relations::Iterator{protozero::pbf_message{nullptr, 0}, 0, relation}; - return Relations::Iterator{protozero::pbf_message{nullptr, 0}, -1}; + return Relations::Iterator{protozero::pbf_message{nullptr, 0}, -1, relation}; } -HeaderBlock PbfReader::readHeaderFromFile(std::istream& input) { - PbfReader::BlobHeader bh = PbfReader::readBlobHeader(input); - protozero::data_view blob = PbfReader::readBlob(bh.datasize, input); - PbfReader::HeaderBlock header = PbfReader::readHeaderBlock(blob); +HeaderBlock PbfReader::PbfReader::readHeaderFromFile(std::istream& input) { + BlobHeader bh = readBlobHeader(input); + protozero::data_view blob = readBlob(bh.datasize, input); + HeaderBlock header = readHeaderBlock(blob); return header; } diff --git a/src/read_pbf.cpp b/src/read_pbf.cpp index b58f8705..28acaaea 100644 --- a/src/read_pbf.cpp +++ b/src/read_pbf.cpp @@ -17,6 +17,9 @@ const std::string OptionSortTypeThenID = "Sort.Type_then_ID"; const std::string OptionLocationsOnWays = "LocationsOnWays"; std::atomic blocksProcessed(0), blocksToProcess(0); +// Thread-local so that we can re-use buffers during parsing. +thread_local PbfReader::PbfReader reader; + PbfProcessor::PbfProcessor(OSMStore &osmStore) : osmStore(osmStore) { } @@ -271,8 +274,8 @@ bool PbfProcessor::ReadBlock( { infile.seekg(blockMetadata.offset); - protozero::data_view blob = PbfReader::readBlob(blockMetadata.length, infile); - PbfReader::PrimitiveBlock& pb = PbfReader::readPrimitiveBlock(blob); + protozero::data_view blob = reader.readBlob(blockMetadata.length, infile); + PbfReader::PrimitiveBlock& pb = reader.readPrimitiveBlock(blob); if (infile.eof()) { return true; } @@ -375,8 +378,8 @@ bool blockHasPrimitiveGroupSatisfying( // We may have previously read to EOF, so clear the internal error state infile.clear(); infile.seekg(block.offset); - protozero::data_view blob = PbfReader::readBlob(block.length, infile); - PbfReader::PrimitiveBlock pb = PbfReader::readPrimitiveBlock(blob); + protozero::data_view blob = reader.readBlob(block.length, infile); + PbfReader::PrimitiveBlock pb = reader.readPrimitiveBlock(blob); if (infile.eof()) { throw std::runtime_error("blockHasPrimitiveGroupSatisfying got unexpected eof"); @@ -406,7 +409,7 @@ int PbfProcessor::ReadPbfFile( // ---- Read PBF osmStore.clear(); - PbfReader::HeaderBlock block = PbfReader::readHeaderFromFile(*infile); + PbfReader::HeaderBlock block = reader.readHeaderFromFile(*infile); bool locationsOnWays = block.optionalFeatures.find(OptionLocationsOnWays) != block.optionalFeatures.end(); if (locationsOnWays) { std::cout << ".osm.pbf file has locations on ways" << std::endl; @@ -418,7 +421,7 @@ int PbfProcessor::ReadPbfFile( // its meant to be an opaque token useful only for seeking. size_t filesize = 0; while (true) { - PbfReader::BlobHeader bh = PbfReader::readBlobHeader(*infile); + PbfReader::BlobHeader bh = reader.readBlobHeader(*infile); filesize += bh.datasize; if (infile->eof()) { break; @@ -634,7 +637,7 @@ int ReadPbfBoundingBox(const std::string &inputFile, double &minLon, double &max { fstream infile(inputFile, ios::in | ios::binary); if (!infile) { cerr << "Couldn't open .pbf file " << inputFile << endl; return -1; } - auto header = PbfReader::readHeaderFromFile(infile); + auto header = reader.readHeaderFromFile(infile); if (header.hasBbox) { hasClippingBox = true; minLon = header.bbox.minLon; @@ -648,7 +651,7 @@ int ReadPbfBoundingBox(const std::string &inputFile, double &minLon, double &max bool PbfHasOptionalFeature(const std::string& inputFile, const std::string& feature) { std::ifstream infile(inputFile, std::ifstream::in); - auto header = PbfReader::readHeaderFromFile(infile); + auto header = reader.readHeaderFromFile(infile); infile.close(); return header.optionalFeatures.find(feature) != header.optionalFeatures.end(); } diff --git a/test/pbf_reader.test.cpp b/test/pbf_reader.test.cpp index 2b09f064..8d4c8fad 100644 --- a/test/pbf_reader.test.cpp +++ b/test/pbf_reader.test.cpp @@ -12,9 +12,10 @@ MU_TEST(test_pbf_reader) { // filename = "/home/cldellow/Downloads/nova-scotia-latest.osm.pbf"; std::ifstream monaco(filename, std::ifstream::in); - PbfReader::BlobHeader bh = PbfReader::readBlobHeader(monaco); - protozero::data_view blob = PbfReader::readBlob(bh.datasize, monaco); - PbfReader::HeaderBlock header = PbfReader::readHeaderBlock(blob); + PbfReader::PbfReader reader; + PbfReader::BlobHeader bh = reader.readBlobHeader(monaco); + protozero::data_view blob = reader.readBlob(bh.datasize, monaco); + PbfReader::HeaderBlock header = reader.readHeaderBlock(blob); mu_check(header.hasBbox); mu_check(header.optionalFeatures.size() == 1); @@ -29,15 +30,15 @@ MU_TEST(test_pbf_reader) { bool foundNode = false, foundWay = false, foundRelation = false; int blocks = 0, groups = 0, strings = 0, nodes = 0, ways = 0, relations = 0; while (!monaco.eof()) { - bh = PbfReader::readBlobHeader(monaco); + bh = reader.readBlobHeader(monaco); if (bh.type == "eof") break; blocks++; - blob = PbfReader::readBlob(bh.datasize, monaco); + blob = reader.readBlob(bh.datasize, monaco); - PbfReader::PrimitiveBlock pb = PbfReader::readPrimitiveBlock(blob); + PbfReader::PrimitiveBlock pb = reader.readPrimitiveBlock(blob); for (const auto str : pb.stringTable) { if (strings == 200) { From 4d4a808352a2015a4a58e5e7c0c93fc17266efc0 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 24 Dec 2023 00:22:44 -0500 Subject: [PATCH 58/81] make things private --- include/pbf_reader.h | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/include/pbf_reader.h b/include/pbf_reader.h index 4f308683..9af930c5 100644 --- a/include/pbf_reader.h +++ b/include/pbf_reader.h @@ -167,15 +167,20 @@ namespace PbfReader { bool operator!=(Iterator& other) const; void operator++(); PbfReader::Way& operator*(); + + private: void readWay(protozero::data_view data); }; - PrimitiveGroup* pg; - Way& way; + Ways(PrimitiveGroup* pg, Way& way): pg(pg), way(way) {} Iterator begin(); Iterator end(); bool empty(); + private: + friend PrimitiveGroup; + PrimitiveGroup* pg; + Way& way; }; struct Relations { @@ -187,15 +192,21 @@ namespace PbfReader { bool operator!=(Iterator& other) const; void operator++(); PbfReader::Relation& operator*(); + + private: void readRelation(protozero::data_view data); }; - PrimitiveGroup* pg; - Relation& relation; + Relations(PrimitiveGroup* pg, Relation& relation): pg(pg), relation(relation) {} Iterator begin(); Iterator end(); bool empty(); + + private: + friend PrimitiveGroup; + PrimitiveGroup* pg; + Relation& relation; }; struct PrimitiveGroup { @@ -225,6 +236,7 @@ namespace PbfReader { }; + class PbfReader; struct PrimitiveBlock { struct PrimitiveGroups { struct Iterator { @@ -238,19 +250,22 @@ namespace PbfReader { PrimitiveGroup& operator*(); }; - std::vector* groups; + PrimitiveGroups(): groups(nullptr) {} PrimitiveGroups(std::vector& groups): groups(&groups) {} Iterator begin(); Iterator end(); + + private: + std::vector* groups; }; std::vector stringTable; PrimitiveGroups& groups(); - // Not meant to be called directly by client code. + private: + friend PbfReader; std::vector internalGroups; - // This is a hack, but my C++-fu isn't enough to know how to avoid it. PrimitiveGroups groupsImpl; }; From 1d0bc17c8a6f6c8bce807f5415d6fd007e1c56e6 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 24 Dec 2023 00:25:02 -0500 Subject: [PATCH 59/81] simplify iterators --- src/pbf_reader.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp index 6ca64087..8239ec89 100644 --- a/src/pbf_reader.cpp +++ b/src/pbf_reader.cpp @@ -466,7 +466,7 @@ bool PbfReader::Ways::empty() { } PbfReader::Ways::Iterator PbfReader::Ways::begin() { if (pg->type() != PrimitiveGroupType::Way) - return Ways::Iterator{protozero::pbf_message{nullptr, 0}, 0, way}; + return Ways::Iterator{protozero::pbf_message{nullptr, 0}, -1, way}; protozero::pbf_message message{pg->getDataView()}; if (message.next()) { @@ -479,9 +479,6 @@ PbfReader::Ways::Iterator PbfReader::Ways::begin() { return Ways::Iterator{message, -1, way}; } PbfReader::Ways::Iterator PbfReader::Ways::end() { - if (pg->type() != PrimitiveGroupType::Way) - return Ways::Iterator{protozero::pbf_message{nullptr, 0}, 0, way}; - return Ways::Iterator{protozero::pbf_message{nullptr, 0}, -1, way}; } @@ -569,7 +566,7 @@ bool PbfReader::Relations::empty() { } PbfReader::Relations::Iterator PbfReader::Relations::begin() { if (pg->type() != PrimitiveGroupType::Relation) - return Relations::Iterator{protozero::pbf_message{nullptr, 0}, 0, relation}; + return Relations::Iterator{protozero::pbf_message{nullptr, 0}, -1, relation}; protozero::pbf_message message{pg->getDataView()}; if (message.next()) { @@ -582,9 +579,6 @@ PbfReader::Relations::Iterator PbfReader::Relations::begin() { return Relations::Iterator{message, -1, relation}; } PbfReader::Relations::Iterator PbfReader::Relations::end() { - if (pg->type() != PrimitiveGroupType::Relation) - return Relations::Iterator{protozero::pbf_message{nullptr, 0}, 0, relation}; - return Relations::Iterator{protozero::pbf_message{nullptr, 0}, -1, relation}; } From 6e330f15bc5a856b034cfb290a845cfdd137b972 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 24 Dec 2023 00:26:01 -0500 Subject: [PATCH 60/81] comment out println --- src/pbf_reader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp index 8239ec89..25723807 100644 --- a/src/pbf_reader.cpp +++ b/src/pbf_reader.cpp @@ -50,7 +50,7 @@ BlobHeader PbfReader::PbfReader::readBlobHeader(std::istream& input) { break; default: // ignore data for unknown tags to allow for future extensions - std::cout << "BlobHeader: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + // std::cout << "BlobHeader: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; message.skip(); } } From 09abd3adb1988b71c204082c3567f410eca3efae Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 24 Dec 2023 12:12:00 -0500 Subject: [PATCH 61/81] extract option parsing to own file We'd like to have different defaults based on whether `--store` is present. Now that option parsing will have some more complex logic, let's pull it into its own class so it can be more easily tested. --- CMakeLists.txt | 1 + Makefile | 6 +- include/helpers.h | 3 +- include/options_parser.h | 54 ++++++++++++ include/shared_data.h | 7 +- src/helpers.cpp | 3 +- src/options_parser.cpp | 100 ++++++++++++++++++++++ src/shared_data.cpp | 2 +- src/tile_worker.cpp | 4 +- src/tilemaker.cpp | 160 +++++++++++++---------------------- test/options_parser.test.cpp | 66 +++++++++++++++ 11 files changed, 294 insertions(+), 112 deletions(-) create mode 100644 include/options_parser.h create mode 100644 src/options_parser.cpp create mode 100644 test/options_parser.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8819b5e2..80a76e7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ file(GLOB tilemaker_src_files src/mbtiles.cpp src/mmap_allocator.cpp src/node_stores.cpp + src/options_parser.cpp src/osm_lua_processing.cpp src/osm_mem_tiles.cpp src/osm_store.cpp diff --git a/Makefile b/Makefile index d0305cce..d44245ae 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,7 @@ tilemaker: \ src/mbtiles.o \ src/mmap_allocator.o \ src/node_stores.o \ + src/options_parser.o \ src/osm_lua_processing.o \ src/osm_mem_tiles.o \ src/osm_store.o \ @@ -152,6 +153,10 @@ test_deque_map: \ test/deque_map.test.o $(CXX) $(CXXFLAGS) -o test.deque_map $^ $(INC) $(LIB) $(LDFLAGS) && ./test.deque_map +test_options_parser: \ + src/options_parser.o \ + test/options_parser.test.o + $(CXX) $(CXXFLAGS) -o test.options_parser $^ $(INC) $(LIB) $(LDFLAGS) && ./test.options_parser test_pooled_string: \ src/mmap_allocator.o \ @@ -168,7 +173,6 @@ test_sorted_node_store: \ test/sorted_node_store.test.o $(CXX) $(CXXFLAGS) -o test.sorted_node_store $^ $(INC) $(LIB) $(LDFLAGS) && ./test.sorted_node_store - test_sorted_way_store: \ src/external/streamvbyte_decode.o \ src/external/streamvbyte_encode.o \ diff --git a/include/helpers.h b/include/helpers.h index 7cb9c027..029a801d 100644 --- a/include/helpers.h +++ b/include/helpers.h @@ -3,7 +3,8 @@ #define _HELPERS_H #include -#include "geom.h" +#include +#include // General helper routines diff --git a/include/options_parser.h b/include/options_parser.h new file mode 100644 index 00000000..12b416dd --- /dev/null +++ b/include/options_parser.h @@ -0,0 +1,54 @@ +#ifndef OPTIONS_PARSER_H +#define OPTIONS_PARSER_H + +#include +#include +#include + +namespace OptionsParser { + struct OptionException : std::exception { + OptionException(std::string message): message(message) {} + + /// Returns the explanatory string. + const char* what() const noexcept override { + return message.data(); + } + + private: + std::string message; + }; + + enum class OutputMode: char { File = 0, MBTiles = 1, PMTiles = 2 }; + + struct OsmOptions { + std::string storeFile; + bool compact = false; + bool skipIntegrity = false; + bool uncompressedNodes = false; + bool uncompressedWays = false; + bool materializeGeometries = false; + bool shardStores = false; + }; + + struct Options { + std::vector inputFiles; + std::string luaFile; + std::string jsonFile; + uint threadNum = 0; + std::string outputFile; + std::string bbox; + + OsmOptions osm; + bool showHelp = false; + bool verbose = false; + bool mergeSqlite = false; + bool mapsplit = false; + OutputMode outputMode = OutputMode::File; + bool logTileTimings = false; + }; + + Options parse(const int argc, const char* argv[]); + void showHelp(); +}; + +#endif diff --git a/include/shared_data.h b/include/shared_data.h index 23ba9a06..45c6e34b 100644 --- a/include/shared_data.h +++ b/include/shared_data.h @@ -7,6 +7,7 @@ #include "rapidjson/document.h" +#include "options_parser.h" #include "osm_store.h" #include "output_object.h" #include "mbtiles.h" @@ -61,10 +62,6 @@ class LayerDefinition { std::string serialiseToJSON() const; }; -const int OUTPUT_FILE = 0; -const int OUTPUT_MBTILES = 1; -const int OUTPUT_PMTILES = 2; - ///\brief Config read from JSON to control behavior of program class Config { @@ -91,7 +88,7 @@ class SharedData { public: const class LayerDefinition &layers; - int outputMode; + OptionsParser::OutputMode outputMode; bool mergeSqlite; MBTiles mbtiles; PMTiles pmtiles; diff --git a/src/helpers.cpp b/src/helpers.cpp index 444ddcf0..4af04612 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "helpers.h" @@ -11,7 +13,6 @@ #define MOD_GZIP_ZLIB_CFACTOR 9 #define MOD_GZIP_ZLIB_BSIZE 8096 -namespace geom = boost::geometry; using namespace std; // Bounding box string parsing diff --git a/src/options_parser.cpp b/src/options_parser.cpp new file mode 100644 index 00000000..f49c5129 --- /dev/null +++ b/src/options_parser.cpp @@ -0,0 +1,100 @@ +#include "options_parser.h" + +#include +#include +#include +#include +#include "helpers.h" + +#ifndef TM_VERSION +#define TM_VERSION (version not set) +#endif +#define STR1(x) #x +#define STR(x) STR1(x) + +using namespace std; +namespace po = boost::program_options; + +po::options_description getParser(OptionsParser::Options& options) { + po::options_description desc("tilemaker " STR(TM_VERSION) "\nConvert OpenStreetMap .pbf files into vector tiles\n\nAvailable options"); + desc.add_options() + ("help", "show help message") + ("input", po::value< vector >(&options.inputFiles), "source .osm.pbf file") + ("output", po::value< string >(&options.outputFile), "target directory or .mbtiles/.pmtiles file") + ("bbox", po::value< string >(&options.bbox), "bounding box to use if input file does not have a bbox header set, example: minlon,minlat,maxlon,maxlat") + ("merge" ,po::bool_switch(&options.mergeSqlite), "merge with existing .mbtiles (overwrites otherwise)") + ("config", po::value< string >(&options.jsonFile)->default_value("config.json"), "config JSON file") + ("process",po::value< string >(&options.luaFile)->default_value("process.lua"), "tag-processing Lua file") + ("store", po::value< string >(&options.osm.storeFile), "temporary storage for node/ways/relations data") + ("compact",po::bool_switch(&options.osm.compact), "Reduce overall memory usage (compact mode).\nNOTE: This requires the input to be renumbered (osmium renumber)") + ("no-compress-nodes", po::bool_switch(&options.osm.uncompressedNodes), "Store nodes uncompressed") + ("no-compress-ways", po::bool_switch(&options.osm.uncompressedWays), "Store ways uncompressed") + ("materialize-geometries", po::bool_switch(&options.osm.materializeGeometries), "Materialize geometries - faster, but requires more memory") + ("shard-stores", po::bool_switch(&options.osm.shardStores), "Shard stores - use an alternate reading/writing strategy for low-memory machines") + ("verbose",po::bool_switch(&options.verbose), "verbose error output") + ("skip-integrity",po::bool_switch(&options.osm.skipIntegrity), "don't enforce way/node integrity") + ("log-tile-timings", po::bool_switch(&options.logTileTimings), "log how long each tile takes") + ("threads",po::value< uint >(&options.threadNum)->default_value(0), "number of threads (automatically detected if 0)"); + po::options_description performance("Performance options"); + performance.add_options() + ("help-module", po::value(), + "produce a help for a given module") + ("version", "output the version number") + ; + + desc.add(performance); + return desc; +} + +void OptionsParser::showHelp() { + Options options; + auto parser = getParser(options); + std::cout << parser << std::endl; +} + +OptionsParser::Options OptionsParser::parse(const int argc, const char* argv[]) { + Options options; + po::options_description desc = getParser(options); + po::positional_options_description p; + p.add("input", 1).add("output", 1); + + po::variables_map vm; + try { + po::store(po::command_line_parser(argc, argv).options(desc).positional(p).run(), vm); + } catch (const po::unknown_option& ex) { + throw OptionException{"Unknown option: " + ex.get_option_name()}; + } + po::notify(vm); + + if (vm.count("help")) { + options.showHelp = true; + return options; + } + if (vm.count("output") == 0) { + throw OptionException{ "You must specify an output file or directory. Run with --help to find out more." }; + } + + if (vm.count("input") == 0) { + throw OptionException{ "No source .osm.pbf file supplied" }; + } + + if (ends_with(options.outputFile, ".mbtiles") || ends_with(options.outputFile, ".sqlite")) { + options.outputMode = OutputMode::MBTiles; + } else if (ends_with(options.outputFile, ".pmtiles")) { + options.outputMode = OutputMode::PMTiles; + } + + if (options.threadNum == 0) { + options.threadNum = max(thread::hardware_concurrency(), 1u); + } + + // ---- Check config + if (!boost::filesystem::exists(options.jsonFile)) { + throw OptionException{ "Couldn't open .json config: " + options.jsonFile }; + } + if (!boost::filesystem::exists(options.luaFile)) { + throw OptionException{"Couldn't open .lua script: " + options.luaFile }; + } + + return options; +} diff --git a/src/shared_data.cpp b/src/shared_data.cpp index 78cfe11d..da9787d8 100644 --- a/src/shared_data.cpp +++ b/src/shared_data.cpp @@ -10,7 +10,7 @@ using namespace rapidjson; SharedData::SharedData(Config &configIn, const class LayerDefinition &layers) : layers(layers), config(configIn) { - outputMode=OUTPUT_FILE; + outputMode=OptionsParser::OutputMode::File; mergeSqlite=false; } diff --git a/src/tile_worker.cpp b/src/tile_worker.cpp index cbdd33e3..7951fcaf 100644 --- a/src/tile_worker.cpp +++ b/src/tile_worker.cpp @@ -378,13 +378,13 @@ void outputProc( // Write to file or sqlite string outputdata, compressed; - if (sharedData.outputMode == OUTPUT_MBTILES) { + if (sharedData.outputMode == OptionsParser::OutputMode::MBTiles) { // Write to sqlite tile.SerializeToString(&outputdata); if (sharedData.config.compress) { compressed = compress_string(outputdata, Z_DEFAULT_COMPRESSION, sharedData.config.gzip); } sharedData.mbtiles.saveTile(zoom, bbox.index.x, bbox.index.y, sharedData.config.compress ? &compressed : &outputdata, sharedData.mergeSqlite); - } else if (sharedData.outputMode == OUTPUT_PMTILES) { + } else if (sharedData.outputMode == OptionsParser::OutputMode::PMTiles) { // Write to pmtiles tile.SerializeToString(&outputdata); sharedData.pmtiles.saveTile(zoom, bbox.index.x, bbox.index.y, outputdata); diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index 7825f65e..1c821001 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -48,6 +48,7 @@ #include "osm_lua_processing.h" #include "mbtiles.h" +#include "options_parser.h" #include "shared_data.h" #include "read_pbf.h" #include "read_shp.h" @@ -80,89 +81,46 @@ bool verbose = false; * * Worker threads write the output tiles, and start in the outputProc function. */ -int main(int argc, char* argv[]) { +int main(const int argc, const char* argv[]) { // ---- Read command-line options - vector inputFiles; - string luaFile; - string osmStoreFile; - string jsonFile; - uint threadNum; - string outputFile; - string bbox; - bool _verbose = false, mergeSqlite = false, mapsplit = false, osmStoreCompact = false, skipIntegrity = false, osmStoreUncompressedNodes = false, osmStoreUncompressedWays = false, materializeGeometries = false, shardStores = false; - int outputMode = OUTPUT_FILE; - bool logTileTimings = false; - - po::options_description desc("tilemaker " STR(TM_VERSION) "\nConvert OpenStreetMap .pbf files into vector tiles\n\nAvailable options"); - desc.add_options() - ("help", "show help message") - ("input", po::value< vector >(&inputFiles), "source .osm.pbf file") - ("output", po::value< string >(&outputFile), "target directory or .mbtiles/.pmtiles file") - ("bbox", po::value< string >(&bbox), "bounding box to use if input file does not have a bbox header set, example: minlon,minlat,maxlon,maxlat") - ("merge" ,po::bool_switch(&mergeSqlite), "merge with existing .mbtiles (overwrites otherwise)") - ("config", po::value< string >(&jsonFile)->default_value("config.json"), "config JSON file") - ("process",po::value< string >(&luaFile)->default_value("process.lua"), "tag-processing Lua file") - ("store", po::value< string >(&osmStoreFile), "temporary storage for node/ways/relations data") - ("compact",po::bool_switch(&osmStoreCompact), "Reduce overall memory usage (compact mode).\nNOTE: This requires the input to be renumbered (osmium renumber)") - ("no-compress-nodes", po::bool_switch(&osmStoreUncompressedNodes), "Store nodes uncompressed") - ("no-compress-ways", po::bool_switch(&osmStoreUncompressedWays), "Store ways uncompressed") - ("materialize-geometries", po::bool_switch(&materializeGeometries), "Materialize geometries - faster, but requires more memory") - ("shard-stores", po::bool_switch(&shardStores), "Shard stores - use an alternate reading/writing strategy for low-memory machines") - ("verbose",po::bool_switch(&_verbose), "verbose error output") - ("skip-integrity",po::bool_switch(&skipIntegrity), "don't enforce way/node integrity") - ("log-tile-timings", po::bool_switch(&logTileTimings), "log how long each tile takes") - ("threads",po::value< uint >(&threadNum)->default_value(0), "number of threads (automatically detected if 0)"); - po::positional_options_description p; - p.add("input", 1).add("output", 1); - po::variables_map vm; + OptionsParser::Options options; try { - po::store(po::command_line_parser(argc, argv).options(desc).positional(p).run(), vm); - } catch (const po::unknown_option& ex) { - cerr << "Unknown option: " << ex.get_option_name() << endl; - return -1; + options = OptionsParser::parse(argc, argv); + } catch (OptionsParser::OptionException& e) { + cerr << e.what() << endl; + return 1; } - po::notify(vm); - - if (vm.count("help")) { cout << desc << endl; return 0; } - if (vm.count("output")==0) { cerr << "You must specify an output file or directory. Run with --help to find out more." << endl; return -1; } - if (vm.count("input")==0) { cout << "No source .osm.pbf file supplied" << endl; } - - vector bboxElements = parseBox(bbox); - if (ends_with(outputFile, ".mbtiles") || ends_with(outputFile, ".sqlite")) { outputMode = OUTPUT_MBTILES; } - else if (ends_with(outputFile, ".pmtiles")) { outputMode = OUTPUT_PMTILES; } - if (threadNum == 0) { threadNum = max(thread::hardware_concurrency(), 1u); } - verbose = _verbose; + if (options.showHelp) { OptionsParser::showHelp(); return 0; } + verbose = options.verbose; - // ---- Check config - - if (!boost::filesystem::exists(jsonFile)) { cerr << "Couldn't open .json config: " << jsonFile << endl; return -1; } - if (!boost::filesystem::exists(luaFile )) { cerr << "Couldn't open .lua script: " << luaFile << endl; return -1; } + vector bboxElements = parseBox(options.bbox); // ---- Remove existing .mbtiles if it exists - - if ((outputMode==OUTPUT_MBTILES || outputMode==OUTPUT_PMTILES) && !mergeSqlite && static_cast(std::ifstream(outputFile))) { + if ((options.outputMode == OptionsParser::OutputMode::MBTiles || options.outputMode == OptionsParser::OutputMode::PMTiles) && !options.mergeSqlite && static_cast(std::ifstream(options.outputFile))) { cout << "Output file exists, will overwrite (Ctrl-C to abort"; - if (outputMode==OUTPUT_MBTILES) cout << ", rerun with --merge to keep"; + if (options.outputMode == OptionsParser::OutputMode::MBTiles) cout << ", rerun with --merge to keep"; cout << ")" << endl; std::this_thread::sleep_for(std::chrono::milliseconds(2000)); - if (remove(outputFile.c_str()) != 0) { + if (remove(options.outputFile.c_str()) != 0) { cerr << "Couldn't remove existing file" << endl; return 0; } - } else if (mergeSqlite && outputMode!=OUTPUT_MBTILES) { + } else if (options.mergeSqlite && options.outputMode != OptionsParser::OutputMode::MBTiles) { cerr << "--merge only works with .mbtiles" << endl; return 0; - } else if (mergeSqlite && !static_cast(std::ifstream(outputFile))) { + } else if (options.mergeSqlite && !static_cast(std::ifstream(options.outputFile))) { cout << "--merge specified but .mbtiles file doesn't already exist, ignoring" << endl; - mergeSqlite = false; + options.mergeSqlite = false; } + // ---- Read bounding box from first .pbf (if there is one) or mapsplit file bool hasClippingBox = false; Box clippingBox; + bool mapsplit = false; MBTiles mapsplitFile; double minLon=0.0, maxLon=0.0, minLat=0.0, maxLat=0.0; if (!bboxElements.empty()) { @@ -172,14 +130,14 @@ int main(int argc, char* argv[]) { maxLon = bboxElementFromStr(bboxElements.at(2)); maxLat = bboxElementFromStr(bboxElements.at(3)); - } else if (inputFiles.size()==1 && (ends_with(inputFiles[0], ".mbtiles") || ends_with(inputFiles[0], ".sqlite") || ends_with(inputFiles[0], ".msf"))) { + } else if (options.inputFiles.size()==1 && (ends_with(options.inputFiles[0], ".mbtiles") || ends_with(options.inputFiles[0], ".sqlite") || ends_with(options.inputFiles[0], ".msf"))) { mapsplit = true; - mapsplitFile.openForReading(inputFiles[0]); + mapsplitFile.openForReading(options.inputFiles[0]); mapsplitFile.readBoundingBox(minLon, maxLon, minLat, maxLat); hasClippingBox = true; - } else if (inputFiles.size()>0) { - int ret = ReadPbfBoundingBox(inputFiles[0], minLon, maxLon, minLat, maxLat, hasClippingBox); + } else if (options.inputFiles.size()>0) { + int ret = ReadPbfBoundingBox(options.inputFiles[0], minLon, maxLon, minLat, maxLat, hasClippingBox); if(ret != 0) return ret; } @@ -193,7 +151,7 @@ int main(int argc, char* argv[]) { rapidjson::Document jsonConfig; class Config config; try { - FILE* fp = fopen(jsonFile.c_str(), "r"); + FILE* fp = fopen(options.jsonFile.c_str(), "r"); char readBuffer[65536]; rapidjson::FileReadStream is(fp, readBuffer, sizeof(readBuffer)); jsonConfig.ParseStream(is); @@ -214,21 +172,21 @@ int main(int argc, char* argv[]) { bool allPbfsHaveSortTypeThenID = true; bool anyPbfHasLocationsOnWays = false; - for (const std::string& file: inputFiles) { + for (const std::string& file: options.inputFiles) { if (ends_with(file, ".pbf")) { allPbfsHaveSortTypeThenID = allPbfsHaveSortTypeThenID && PbfHasOptionalFeature(file, OptionSortTypeThenID); anyPbfHasLocationsOnWays = anyPbfHasLocationsOnWays || PbfHasOptionalFeature(file, OptionLocationsOnWays); } } - auto createNodeStore = [allPbfsHaveSortTypeThenID, osmStoreCompact, osmStoreUncompressedNodes]() { - if (osmStoreCompact) { + auto createNodeStore = [allPbfsHaveSortTypeThenID, options]() { + if (options.osm.compact) { std::shared_ptr rv = make_shared(); return rv; } if (allPbfsHaveSortTypeThenID) { - std::shared_ptr rv = make_shared(!osmStoreUncompressedNodes); + std::shared_ptr rv = make_shared(!options.osm.uncompressedNodes); return rv; } std::shared_ptr rv = make_shared(); @@ -237,15 +195,15 @@ int main(int argc, char* argv[]) { shared_ptr nodeStore; - if (shardStores) { + if (options.osm.shardStores) { nodeStore = std::make_shared(createNodeStore); } else { nodeStore = createNodeStore(); } - auto createWayStore = [anyPbfHasLocationsOnWays, allPbfsHaveSortTypeThenID, osmStoreUncompressedWays, &nodeStore]() { + auto createWayStore = [anyPbfHasLocationsOnWays, allPbfsHaveSortTypeThenID, options, &nodeStore]() { if (!anyPbfHasLocationsOnWays && allPbfsHaveSortTypeThenID) { - std::shared_ptr rv = make_shared(!osmStoreUncompressedWays, *nodeStore.get()); + std::shared_ptr rv = make_shared(!options.osm.uncompressedWays, *nodeStore.get()); return rv; } @@ -254,30 +212,30 @@ int main(int argc, char* argv[]) { }; shared_ptr wayStore; - if (shardStores) { + if (options.osm.shardStores) { wayStore = std::make_shared(createWayStore, *nodeStore.get()); } else { wayStore = createWayStore(); } OSMStore osmStore(*nodeStore.get(), *wayStore.get()); - osmStore.use_compact_store(osmStoreCompact); - osmStore.enforce_integrity(!skipIntegrity); - if(!osmStoreFile.empty()) { - std::cout << "Using osm store file: " << osmStoreFile << std::endl; - osmStore.open(osmStoreFile); + osmStore.use_compact_store(options.osm.compact); + osmStore.enforce_integrity(!options.osm.skipIntegrity); + if(!options.osm.storeFile.empty()) { + std::cout << "Using osm store file: " << options.osm.storeFile << std::endl; + osmStore.open(options.osm.storeFile); } AttributeStore attributeStore; class LayerDefinition layers(config.layers); - class OsmMemTiles osmMemTiles(threadNum, config.baseZoom, config.includeID, *nodeStore, *wayStore); - class ShpMemTiles shpMemTiles(threadNum, config.baseZoom); + class OsmMemTiles osmMemTiles(options.threadNum, config.baseZoom, config.includeID, *nodeStore, *wayStore); + class ShpMemTiles shpMemTiles(options.threadNum, config.baseZoom); osmMemTiles.open(); shpMemTiles.open(); - OsmLuaProcessing osmLuaProcessing(osmStore, config, layers, luaFile, - shpMemTiles, osmMemTiles, attributeStore, materializeGeometries); + OsmLuaProcessing osmLuaProcessing(osmStore, config, layers, options.luaFile, + shpMemTiles, osmMemTiles, attributeStore, options.osm.materializeGeometries); // ---- Load external shp files @@ -295,7 +253,7 @@ int main(int argc, char* argv[]) { readShapefile(clippingBox, layers, config.baseZoom, layerNum, - threadNum, + options.threadNum, shpMemTiles, osmLuaProcessing); } } @@ -312,7 +270,7 @@ int main(int argc, char* argv[]) { std::vector sortOrders = layers.getSortOrders(); if (!mapsplit) { - for (auto inputFile : inputFiles) { + for (auto inputFile : options.inputFiles) { cout << "Reading .pbf " << inputFile << endl; ifstream infile(inputFile, ios::in | ios::binary); if (!infile) { cerr << "Couldn't open .pbf file " << inputFile << endl; return -1; } @@ -322,13 +280,13 @@ int main(int argc, char* argv[]) { nodeStore->shards(), hasSortTypeThenID, nodeKeys, - threadNum, + options.threadNum, [&]() { thread_local std::shared_ptr pbfStream(new ifstream(inputFile, ios::in | ios::binary)); return pbfStream; }, [&]() { - thread_local std::shared_ptr osmLuaProcessing(new OsmLuaProcessing(osmStore, config, layers, luaFile, shpMemTiles, osmMemTiles, attributeStore, materializeGeometries)); + thread_local std::shared_ptr osmLuaProcessing(new OsmLuaProcessing(osmStore, config, layers, options.luaFile, shpMemTiles, osmMemTiles, attributeStore, options.osm.materializeGeometries)); return osmLuaProcessing; }, *nodeStore, @@ -343,16 +301,16 @@ int main(int argc, char* argv[]) { // ---- Initialise SharedData SourceList sources = {&osmMemTiles, &shpMemTiles}; class SharedData sharedData(config, layers); - sharedData.outputFile = outputFile; - sharedData.outputMode = outputMode; - sharedData.mergeSqlite = mergeSqlite; + sharedData.outputFile = options.outputFile; + sharedData.outputMode = options.outputMode; + sharedData.mergeSqlite = options.mergeSqlite; // ---- Initialise mbtiles/pmtiles if required - if (sharedData.outputMode==OUTPUT_MBTILES) { + if (sharedData.outputMode == OptionsParser::OutputMode::MBTiles) { sharedData.mbtiles.openForWriting(sharedData.outputFile); sharedData.writeMBTilesProjectData(); - } else if (sharedData.outputMode==OUTPUT_PMTILES) { + } else if (sharedData.outputMode == OptionsParser::OutputMode::PMTiles) { sharedData.pmtiles.open(sharedData.outputFile); } @@ -394,7 +352,7 @@ int main(int argc, char* argv[]) { return make_unique(pbf.data(), pbf.size(), ios::in | ios::binary); }, [&]() { - return std::make_unique(osmStore, config, layers, luaFile, shpMemTiles, osmMemTiles, attributeStore, materializeGeometries); + return std::make_unique(osmStore, config, layers, options.luaFile, shpMemTiles, osmMemTiles, attributeStore, options.osm.materializeGeometries); }, *nodeStore, *wayStore @@ -405,7 +363,7 @@ int main(int argc, char* argv[]) { } // Launch the pool with threadNum threads - boost::asio::thread_pool pool(threadNum); + boost::asio::thread_pool pool(options.threadNum); // Mutex is hold when IO is performed std::mutex io_mutex; @@ -414,7 +372,7 @@ int main(int argc, char* argv[]) { std::atomic tilesWritten(0); for (auto source : sources) { - source->finalize(threadNum); + source->finalize(options.threadNum); } // tiles by zoom level @@ -435,7 +393,7 @@ int main(int argc, char* argv[]) { } // For large areas (arbitrarily defined as 100 z6 tiles), use a dense index for pmtiles - if (coveredZ6Tiles.size()>100 && outputMode==OUTPUT_PMTILES) { + if (coveredZ6Tiles.size()>100 && options.outputMode == OptionsParser::OutputMode::PMTiles) { std::cout << "Using dense index for .pmtiles" << std::endl; sharedData.pmtiles.isSparse = false; } @@ -561,7 +519,7 @@ int main(int argc, char* argv[]) { return false; }, - threadNum); + options.threadNum); std::size_t batchSize = 0; for(std::size_t startIndex = 0; startIndex < tileCoordinates.size(); startIndex += batchSize) { @@ -592,7 +550,7 @@ int main(int argc, char* argv[]) { #ifdef CLOCK_MONOTONIC timespec start, end; - if (logTileTimings) + if (options.logTileTimings) clock_gettime(CLOCK_MONOTONIC, &start); #endif @@ -603,7 +561,7 @@ int main(int argc, char* argv[]) { outputProc(sharedData, sources, attributeStore, data, coords, zoom); #ifdef CLOCK_MONOTONIC - if (logTileTimings) { + if (options.logTileTimings) { clock_gettime(CLOCK_MONOTONIC, &end); uint64_t tileNs = 1e9 * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; std::string output = "z" + std::to_string(zoom) + "/" + std::to_string(coords.x) + "/" + std::to_string(coords.y) + " took " + std::to_string(tileNs/1e6) + " ms"; @@ -612,7 +570,7 @@ int main(int argc, char* argv[]) { #endif } - if (logTileTimings) { + if (options.logTileTimings) { const std::lock_guard lock(io_mutex); std::cout << std::endl; for (const auto& output : tileTimings) @@ -642,10 +600,10 @@ int main(int argc, char* argv[]) { // ---- Close tileset - if (outputMode==OUTPUT_MBTILES) { + if (options.outputMode == OptionsParser::OutputMode::MBTiles) { sharedData.writeMBTilesMetadata(jsonConfig); sharedData.mbtiles.closeForWriting(); - } else if (outputMode==OUTPUT_PMTILES) { + } else if (options.outputMode == OptionsParser::OutputMode::PMTiles) { sharedData.writePMTilesBounds(); std::string metadata = sharedData.pmTilesMetadata(); sharedData.pmtiles.close(metadata); diff --git a/test/options_parser.test.cpp b/test/options_parser.test.cpp new file mode 100644 index 00000000..77b4874d --- /dev/null +++ b/test/options_parser.test.cpp @@ -0,0 +1,66 @@ +#include +#include "external/minunit.h" +#include "options_parser.h" + +const char* PROGRAM_NAME = "./tilemaker"; +using namespace OptionsParser; + +Options parse(std::vector& args) { + const char* argv[100]; + + argv[0] = PROGRAM_NAME; + for(int i = 0; i < args.size(); i++) + argv[1 + i] = args[i].data(); + + return parse(1 + args.size(), argv); +} + +#define ASSERT_THROWS(MESSAGE, ...) \ +{ \ + std::vector args = { __VA_ARGS__ }; \ + bool threw = false; \ + try { \ + auto opts = parse(args); \ + } catch(OptionsParser::OptionException& e) { \ + threw = std::string(e.what()).find(MESSAGE) != std::string::npos; \ + } \ + if (!threw) mu_check((std::string("expected exception with ") + MESSAGE).empty()); \ +} + +MU_TEST(test_options_parser) { + // No args is invalid. + ASSERT_THROWS("You must specify an output file"); + + // Output without input is invalid + ASSERT_THROWS("No source .osm.pbf", "--output", "foo.mbtiles"); + + // You can ask for --help. + { + std::vector args = {"--help"}; + auto opts = parse(args); + mu_check(opts.showHelp); + } + + // Minimal valid is output and input + { + std::vector args = {"--output", "foo.mbtiles", "--input", "ontario.pbf"}; + auto opts = parse(args); + mu_check(opts.inputFiles.size() == 1); + mu_check(opts.inputFiles[0] == "ontario.pbf"); + mu_check(opts.outputFile == "foo.mbtiles"); + mu_check(opts.outputMode == OutputMode::MBTiles); + } + + ASSERT_THROWS("Couldn't open .json config", "--input", "foo", "--output", "bar", "--config", "nonexistent-config.json"); + ASSERT_THROWS("Couldn't open .lua script", "--input", "foo", "--output", "bar", "--process", "nonexistent-script.lua"); +} + +MU_TEST_SUITE(test_suite_options_parser) { + MU_RUN_TEST(test_options_parser); +} + +int main() { + MU_RUN_SUITE(test_suite_options_parser); + MU_REPORT(); + return MU_EXIT_CODE; +} From 48305a4981e86b666b9da9ca4f60977400fa7559 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 24 Dec 2023 13:00:24 -0500 Subject: [PATCH 62/81] use sensible defaults based on presence of --store --- include/options_parser.h | 1 + src/options_parser.cpp | 31 ++++++++++++++++++++----------- test/options_parser.test.cpp | 28 ++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/include/options_parser.h b/include/options_parser.h index 12b416dd..d9441aef 100644 --- a/include/options_parser.h +++ b/include/options_parser.h @@ -22,6 +22,7 @@ namespace OptionsParser { struct OsmOptions { std::string storeFile; + bool fast = false; bool compact = false; bool skipIntegrity = false; bool uncompressedNodes = false; diff --git a/src/options_parser.cpp b/src/options_parser.cpp index f49c5129..274fd848 100644 --- a/src/options_parser.cpp +++ b/src/options_parser.cpp @@ -25,21 +25,19 @@ po::options_description getParser(OptionsParser::Options& options) { ("merge" ,po::bool_switch(&options.mergeSqlite), "merge with existing .mbtiles (overwrites otherwise)") ("config", po::value< string >(&options.jsonFile)->default_value("config.json"), "config JSON file") ("process",po::value< string >(&options.luaFile)->default_value("process.lua"), "tag-processing Lua file") - ("store", po::value< string >(&options.osm.storeFile), "temporary storage for node/ways/relations data") - ("compact",po::bool_switch(&options.osm.compact), "Reduce overall memory usage (compact mode).\nNOTE: This requires the input to be renumbered (osmium renumber)") - ("no-compress-nodes", po::bool_switch(&options.osm.uncompressedNodes), "Store nodes uncompressed") - ("no-compress-ways", po::bool_switch(&options.osm.uncompressedWays), "Store ways uncompressed") - ("materialize-geometries", po::bool_switch(&options.osm.materializeGeometries), "Materialize geometries - faster, but requires more memory") - ("shard-stores", po::bool_switch(&options.osm.shardStores), "Shard stores - use an alternate reading/writing strategy for low-memory machines") ("verbose",po::bool_switch(&options.verbose), "verbose error output") ("skip-integrity",po::bool_switch(&options.osm.skipIntegrity), "don't enforce way/node integrity") - ("log-tile-timings", po::bool_switch(&options.logTileTimings), "log how long each tile takes") - ("threads",po::value< uint >(&options.threadNum)->default_value(0), "number of threads (automatically detected if 0)"); + ("log-tile-timings", po::bool_switch(&options.logTileTimings), "log how long each tile takes"); po::options_description performance("Performance options"); performance.add_options() - ("help-module", po::value(), - "produce a help for a given module") - ("version", "output the version number") + ("store", po::value< string >(&options.osm.storeFile), "temporary storage for node/ways/relations data") + ("fast", po::bool_switch(&options.osm.fast), "prefer speed at the expense of memory") + ("compact",po::bool_switch(&options.osm.compact), "use faster data structure for node lookups\nNOTE: This requires the input to be renumbered (osmium renumber)") + ("no-compress-nodes", po::bool_switch(&options.osm.uncompressedNodes), "store nodes uncompressed") + ("no-compress-ways", po::bool_switch(&options.osm.uncompressedWays), "store ways uncompressed") + ("materialize-geometries", po::bool_switch(&options.osm.materializeGeometries), "materialize geometries") + ("shard-stores", po::bool_switch(&options.osm.shardStores), "use an alternate reading/writing strategy for low-memory machines") + ("threads",po::value< uint >(&options.threadNum)->default_value(0), "number of threads (automatically detected if 0)") ; desc.add(performance); @@ -54,6 +52,7 @@ void OptionsParser::showHelp() { OptionsParser::Options OptionsParser::parse(const int argc, const char* argv[]) { Options options; + po::options_description desc = getParser(options); po::positional_options_description p; p.add("input", 1).add("output", 1); @@ -65,6 +64,16 @@ OptionsParser::Options OptionsParser::parse(const int argc, const char* argv[]) throw OptionException{"Unknown option: " + ex.get_option_name()}; } po::notify(vm); + + if (options.osm.storeFile.empty()) { + options.osm.materializeGeometries = true; + } else { + if (options.osm.fast) { + options.osm.materializeGeometries = true; + } else { + options.osm.shardStores = true; + } + } if (vm.count("help")) { options.showHelp = true; diff --git a/test/options_parser.test.cpp b/test/options_parser.test.cpp index 77b4874d..e3af0ad2 100644 --- a/test/options_parser.test.cpp +++ b/test/options_parser.test.cpp @@ -49,6 +49,34 @@ MU_TEST(test_options_parser) { mu_check(opts.inputFiles[0] == "ontario.pbf"); mu_check(opts.outputFile == "foo.mbtiles"); mu_check(opts.outputMode == OutputMode::MBTiles); + mu_check(opts.osm.materializeGeometries); + mu_check(!opts.osm.shardStores); + } + + // --store should optimize for reduced memory + { + std::vector args = {"--output", "foo.mbtiles", "--input", "ontario.pbf", "--store", "/tmp/store"}; + auto opts = parse(args); + mu_check(opts.inputFiles.size() == 1); + mu_check(opts.inputFiles[0] == "ontario.pbf"); + mu_check(opts.outputFile == "foo.mbtiles"); + mu_check(opts.outputMode == OutputMode::MBTiles); + mu_check(opts.osm.storeFile == "/tmp/store"); + mu_check(!opts.osm.materializeGeometries); + mu_check(opts.osm.shardStores); + } + + // --store --fast should optimize for speed + { + std::vector args = {"--output", "foo.mbtiles", "--input", "ontario.pbf", "--store", "/tmp/store", "--fast"}; + auto opts = parse(args); + mu_check(opts.inputFiles.size() == 1); + mu_check(opts.inputFiles[0] == "ontario.pbf"); + mu_check(opts.outputFile == "foo.mbtiles"); + mu_check(opts.outputMode == OutputMode::MBTiles); + mu_check(opts.osm.storeFile == "/tmp/store"); + mu_check(opts.osm.materializeGeometries); + mu_check(!opts.osm.shardStores); } ASSERT_THROWS("Couldn't open .json config", "--input", "foo", "--output", "bar", "--config", "nonexistent-config.json"); From 411b71ee031ca509ce723cfc192354a18f975115 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 24 Dec 2023 13:02:13 -0500 Subject: [PATCH 63/81] improve test coverage --- test/options_parser.test.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/options_parser.test.cpp b/test/options_parser.test.cpp index e3af0ad2..10e09597 100644 --- a/test/options_parser.test.cpp +++ b/test/options_parser.test.cpp @@ -68,12 +68,12 @@ MU_TEST(test_options_parser) { // --store --fast should optimize for speed { - std::vector args = {"--output", "foo.mbtiles", "--input", "ontario.pbf", "--store", "/tmp/store", "--fast"}; + std::vector args = {"--output", "foo.pmtiles", "--input", "ontario.pbf", "--store", "/tmp/store", "--fast"}; auto opts = parse(args); mu_check(opts.inputFiles.size() == 1); mu_check(opts.inputFiles[0] == "ontario.pbf"); - mu_check(opts.outputFile == "foo.mbtiles"); - mu_check(opts.outputMode == OutputMode::MBTiles); + mu_check(opts.outputFile == "foo.pmtiles"); + mu_check(opts.outputMode == OutputMode::PMTiles); mu_check(opts.osm.storeFile == "/tmp/store"); mu_check(opts.osm.materializeGeometries); mu_check(!opts.osm.shardStores); From 3d89a78b5c23e4e333f5702eb86fdf31c405b443 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 24 Dec 2023 13:06:30 -0500 Subject: [PATCH 64/81] fixes --- include/node_stores.h | 6 +++--- include/options_parser.h | 2 +- include/sharded_node_store.h | 2 +- include/sorted_node_store.h | 2 +- src/options_parser.cpp | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/node_stores.h b/include/node_stores.h index 2ef14b70..05d00f4e 100644 --- a/include/node_stores.h +++ b/include/node_stores.h @@ -20,10 +20,10 @@ class BinarySearchNodeStore : public NodeStore LatpLon at(NodeID i) const override; size_t size() const override; void insert(const std::vector& elements) override; - void clear() { + void clear() override { reopen(); } - void batchStart() {} + void batchStart() override {} bool contains(size_t shard, NodeID id) const override; NodeStore& shard(size_t shard) override { return *this; } @@ -56,7 +56,7 @@ class CompactNodeStore : public NodeStore void insert(const std::vector& elements) override; void clear() override; void finalize(size_t numThreads) override {} - void batchStart() {} + void batchStart() override {} // CompactNodeStore has no metadata to know whether or not it contains // a node, so it's not suitable for used in sharded scenarios. diff --git a/include/options_parser.h b/include/options_parser.h index d9441aef..c5307932 100644 --- a/include/options_parser.h +++ b/include/options_parser.h @@ -35,7 +35,7 @@ namespace OptionsParser { std::vector inputFiles; std::string luaFile; std::string jsonFile; - uint threadNum = 0; + uint32_t threadNum = 0; std::string outputFile; std::string bbox; diff --git a/include/sharded_node_store.h b/include/sharded_node_store.h index ef001347..836c34ef 100644 --- a/include/sharded_node_store.h +++ b/include/sharded_node_store.h @@ -15,7 +15,7 @@ class ShardedNodeStore : public NodeStore { size_t size() const override; void batchStart() override; void insert(const std::vector& elements) override; - void clear() { + void clear() override { reopen(); } diff --git a/include/sorted_node_store.h b/include/sorted_node_store.h index e2832df8..61fdfad3 100644 --- a/include/sorted_node_store.h +++ b/include/sorted_node_store.h @@ -66,7 +66,7 @@ class SortedNodeStore : public NodeStore size_t size() const override; void batchStart() override; void insert(const std::vector& elements) override; - void clear() { + void clear() override { reopen(); } diff --git a/src/options_parser.cpp b/src/options_parser.cpp index 274fd848..3ea60798 100644 --- a/src/options_parser.cpp +++ b/src/options_parser.cpp @@ -37,7 +37,7 @@ po::options_description getParser(OptionsParser::Options& options) { ("no-compress-ways", po::bool_switch(&options.osm.uncompressedWays), "store ways uncompressed") ("materialize-geometries", po::bool_switch(&options.osm.materializeGeometries), "materialize geometries") ("shard-stores", po::bool_switch(&options.osm.shardStores), "use an alternate reading/writing strategy for low-memory machines") - ("threads",po::value< uint >(&options.threadNum)->default_value(0), "number of threads (automatically detected if 0)") + ("threads",po::value(&options.threadNum)->default_value(0), "number of threads (automatically detected if 0)") ; desc.add(performance); From 1edbfd6e9b2bd1e21860805f10f4038a99bc4c38 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Sun, 24 Dec 2023 14:00:06 -0500 Subject: [PATCH 65/81] update number of shards to 6 This has no performance impact as we never put anything in the 7th shard, and so we skip doing the 7th pass in the ReadPhase::Ways and ReadPhase::Relations phase. The benefit is only to avoid emitting a noisy log about how the 7th store has 0 entries in it. Timings with 6 shards on Vultr's 16-core machine here: https://gist.github.com/cldellow/77991eb4074f6a0f31766cf901659efb The new peak memory is ~12.2GB. I am a little perplexed -- the runtime on a 16-core server was previously: ``` $ time tilemaker --store /tmp/store --input planet-latest.osm.pbf --output tiles.mbtiles --shard-stores real 195m7.819s user 2473m52.322s sys 73m13.116s ``` But with the most recent commits on this branch, it was: ``` real 118m50.098s user 1531m13.026s sys 34m7.252s ``` This is incredibly suspicious. I also tried re-running commit bbf0957c1eb1cca7e35e1aa36e8a672e22a65034, and got: ``` real 123m15.534s user 1546m25.196s sys 38m17.093s ``` ...so I can't explain why the earlier runs took 195 min. Ideas: - the planet changed between runs, and a horribly broken geometry was fixed - Vultr gives quite different machines for the same class of server - perhaps most likely: I failed to click "CPU-optimized" when picking the earlier server, and got a slow machine the first time, and a fast machine the second time. I'm pretty sure I paid the same $, so I'm not sure I believe this. I don't think I really believe that a 33% reduction in runtime is explained by any of those, though. Anyway, just another thing to be befuddled by. --- src/sharded_node_store.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sharded_node_store.cpp b/src/sharded_node_store.cpp index e9a6dc16..0d915fbd 100644 --- a/src/sharded_node_store.cpp +++ b/src/sharded_node_store.cpp @@ -99,5 +99,5 @@ bool ShardedNodeStore::contains(size_t shard, NodeID id) const { } size_t ShardedNodeStore::shards() const { - return 7; + return 6; } From be682a0bb613641661e76380a2f57c3038dd2fa4 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 25 Dec 2023 09:32:50 -0500 Subject: [PATCH 66/81] try to avoid lock contention on AttributeStore On a 48-core machine, I still see lots of lock contention. AttributeStore:add is one place. Add a thread-local cache that can be consulted without taking the shared lock. The intuition here is that there are 1.3B objects, and 40M attribute sets. Thus, on average, an attribute set is reused 32 times. However, average is probably misleading -- the distribution is likely not uniform, e.g. the median attribute set is probably reused 1-2 times, and some exceptional attribute sets (e.g. `natural=tree` are reused thousands of times). For GB on a 16-core machine, this avoids 27M of 36M locks. --- include/attribute_store.h | 1 + include/deque_map.h | 6 +++++- src/attribute_store.cpp | 44 +++++++++++++++++++++++++++++++++------ test/deque_map.test.cpp | 4 ++++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/include/attribute_store.h b/include/attribute_store.h index 3aea19cf..95c4cabb 100644 --- a/include/attribute_store.h +++ b/include/attribute_store.h @@ -418,6 +418,7 @@ struct AttributeStore { mutable std::vector setsMutex; mutable std::mutex mutex; + std::atomic lookupsUncached; std::atomic lookups; }; diff --git a/include/deque_map.h b/include/deque_map.h index bcb4ddbc..ea57f669 100644 --- a/include/deque_map.h +++ b/include/deque_map.h @@ -95,7 +95,11 @@ class DequeMap { return -1; } - const T& at(uint32_t index) const { + inline const T& operator[](uint32_t index) const { + return objects[index]; + } + + inline const T& at(uint32_t index) const { return objects.at(index); } diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index 6fbacbe9..7ce784b8 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -73,18 +73,18 @@ const AttributePair& AttributePairStore::getPair(uint32_t i) const { if (shard == 0) { if (offset < tlsHotShard.size()) - return tlsHotShard.at(offset); + return tlsHotShard[offset]; { std::lock_guard lock(pairsMutex[0]); tlsHotShard = pairs[0]; } - return tlsHotShard.at(offset); + return tlsHotShard[offset]; } std::lock_guard lock(pairsMutex[shard]); - return pairs[shard].at(offset); + return pairs[shard][offset]; }; const AttributePair& AttributePairStore::getPairUnsafe(uint32_t i) const { @@ -94,7 +94,7 @@ const AttributePair& AttributePairStore::getPairUnsafe(uint32_t i) const { uint32_t shard = i >> (32 - SHARD_BITS); uint32_t offset = i & (~(~0u << (32 - SHARD_BITS))); - return pairs[shard].at(offset); + return pairs[shard][offset]; }; uint32_t AttributePairStore::addPair(AttributePair& pair, bool isHot) { @@ -263,25 +263,54 @@ void AttributeSet::finalize() { } +// Remember recently queried/added sets so that we can return them in the +// future without taking a lock. +thread_local std::vector cachedAttributeSetPointers(64); +thread_local std::vector cachedAttributeSetIndexes(64); + +thread_local uint64_t tlsLookups = 0; +thread_local uint64_t tlsLookupsUncached = 0; AttributeIndex AttributeStore::add(AttributeSet &attributes) { // TODO: there's probably a way to use C++ types to distinguish a finalized // and non-finalized AttributeSet, which would make this safer. attributes.finalize(); size_t hash = attributes.hash(); + + const size_t candidateIndex = hash % cachedAttributeSetPointers.size(); + // Before taking a lock, see if we've seen this attribute set recently. + + tlsLookups++; + if (tlsLookups % 1024 == 0) { + lookups += 1024; + } + + + { + const AttributeSet* candidate = cachedAttributeSetPointers[candidateIndex]; + + if (candidate != nullptr && *candidate == attributes) + return cachedAttributeSetIndexes[candidateIndex]; + } + size_t shard = hash % ATTRIBUTE_SHARDS; // We can't use the top 2 bits (see OutputObject's bitfields) shard = shard >> 2; std::lock_guard lock(setsMutex[shard]); - lookups++; + tlsLookupsUncached++; + if (tlsLookupsUncached % 1024 == 0) + lookupsUncached += 1024; const uint32_t offset = sets[shard].add(attributes); if (offset >= (1 << (32 - SHARD_BITS))) throw std::out_of_range("set shard overflow"); uint32_t rv = (shard << (32 - SHARD_BITS)) + offset; + + cachedAttributeSetPointers[candidateIndex] = &sets[shard][offset]; + cachedAttributeSetIndexes[candidateIndex] = rv; return rv; } @@ -317,7 +346,7 @@ size_t AttributeStore::size() const { } void AttributeStore::reportSize() const { - std::cout << "Attributes: " << size() << " sets from " << lookups.load() << " objects" << std::endl; + std::cout << "Attributes: " << size() << " sets from " << lookups.load() << " objects (" << lookupsUncached.load() << " uncached)" << std::endl; // Print detailed histogram of frequencies of attributes. if (false) { @@ -380,6 +409,9 @@ void AttributeStore::reset() { tlsKeys2IndexSize = 0; tlsHotShard.clear(); + + for (int i = 0; i < cachedAttributeSetPointers.size(); i++) + cachedAttributeSetPointers[i] = nullptr; } void AttributeStore::finalize() { diff --git a/test/deque_map.test.cpp b/test/deque_map.test.cpp index 23a3d3cc..28023542 100644 --- a/test/deque_map.test.cpp +++ b/test/deque_map.test.cpp @@ -25,9 +25,13 @@ MU_TEST(test_deque_map) { mu_check(strs.size() == 4); mu_check(strs.at(0) == "foo"); + mu_check(strs[0] == "foo"); mu_check(strs.at(1) == "bar"); + mu_check(strs[1] == "bar"); mu_check(strs.at(2) == "aardvark"); + mu_check(strs[2] == "aardvark"); mu_check(strs.at(3) == "quux"); + mu_check(strs[3] == "quux"); std::vector rv; for (std::string x : strs) { From c4631a3c43a1a9873ece3576e120aba2d054eb2f Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 25 Dec 2023 09:58:25 -0500 Subject: [PATCH 67/81] RelationScanStore: more granular locks On a 48-core machine, this phase currently achieves only 400% CPU usage, I think due to these locks --- include/osm_store.h | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/include/osm_store.h b/include/osm_store.h index b8607543..5bb74272 100644 --- a/include/osm_store.h +++ b/include/osm_store.h @@ -81,37 +81,39 @@ class RelationScanStore { private: using tag_map_t = boost::container::flat_map; - std::map> relationsForWays; - std::map relationTags; - mutable std::mutex mutex; + std::vector>> relationsForWays; + std::vector> relationTags; + mutable std::vector mutex; public: + RelationScanStore(): relationsForWays(128), relationTags(128), mutex(128) {} void relation_contains_way(WayID relid, WayID wayid) { - std::lock_guard lock(mutex); - relationsForWays[wayid].emplace_back(relid); + const size_t shard = wayid % mutex.size(); + + std::lock_guard lock(mutex[shard]); + relationsForWays[shard][wayid].emplace_back(relid); } void store_relation_tags(WayID relid, const tag_map_t &tags) { - std::lock_guard lock(mutex); - relationTags[relid] = tags; + const size_t shard = relid % mutex.size(); + std::lock_guard lock(mutex[shard]); + relationTags[shard][relid] = tags; } bool way_in_any_relations(WayID wayid) { - return relationsForWays.find(wayid) != relationsForWays.end(); + const size_t shard = wayid % mutex.size(); + return relationsForWays[shard].find(wayid) != relationsForWays[shard].end(); } std::vector relations_for_way(WayID wayid) { - return relationsForWays[wayid]; + const size_t shard = wayid % mutex.size(); + return relationsForWays[shard][wayid]; } std::string get_relation_tag(WayID relid, const std::string &key) { - auto it = relationTags.find(relid); - if (it==relationTags.end()) return ""; + const size_t shard = relid % mutex.size(); + auto it = relationTags[shard].find(relid); + if (it==relationTags[shard].end()) return ""; auto jt = it->second.find(key); if (jt==it->second.end()) return ""; return jt->second; } - void clear() { - std::lock_guard lock(mutex); - relationsForWays.clear(); - relationTags.clear(); - } }; From e8720738fe522a00f7d13b10339fc73955e7a66e Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 25 Dec 2023 10:24:57 -0500 Subject: [PATCH 68/81] AttributePairStore.getPair: add thread-local cache --- include/attribute_store.h | 15 +++++++++++---- src/attribute_store.cpp | 40 +++++++++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/include/attribute_store.h b/include/attribute_store.h index 95c4cabb..98fda7c6 100644 --- a/include/attribute_store.h +++ b/include/attribute_store.h @@ -183,11 +183,15 @@ struct AttributePair { #define SHARD_BITS 14 #define ATTRIBUTE_SHARDS (1 << SHARD_BITS) +class AttributeStore; + class AttributePairStore { public: AttributePairStore(): finalized(false), - pairsMutex(ATTRIBUTE_SHARDS) + pairsMutex(ATTRIBUTE_SHARDS), + lookups(0), + lookupsUncached(0) { // The "hot" shard has a capacity of 64K, the others are unbounded. pairs.push_back(DequeMap(1 << 16)); @@ -202,9 +206,9 @@ class AttributePairStore { const AttributePair& getPairUnsafe(uint32_t i) const; uint32_t addPair(AttributePair& pair, bool isHot); - std::vector> pairs; - private: + friend class AttributeStore; + std::vector> pairs; bool finalized; // We refer to all attribute pairs by index. // @@ -214,6 +218,8 @@ class AttributePairStore { // we suspect will be popular. It only ever has 64KB items, // so that we can reference it with a short. mutable std::vector pairsMutex; + mutable std::atomic lookupsUncached; + mutable std::atomic lookups; }; // AttributeSet is a set of AttributePairs @@ -406,7 +412,8 @@ struct AttributeStore { finalized(false), sets(ATTRIBUTE_SHARDS), setsMutex(ATTRIBUTE_SHARDS), - lookups(0) { + lookups(0), + lookupsUncached(0) { } AttributeKeyStore keyStore; diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index 7ce784b8..e0519179 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -67,6 +67,13 @@ void AttributePair::ensureStringIsOwned() { // AttributePairStore thread_local DequeMap tlsHotShard(1 << 16); +// Remember recently queried/added sets so that we can return them in the +// future without taking a lock. +thread_local std::vector cachedAttributePairPointers(64); +thread_local std::vector cachedAttributePairIndexes(64); + +thread_local uint64_t tlsPairLookups = 0; +thread_local uint64_t tlsPairLookupsUncached = 0; const AttributePair& AttributePairStore::getPair(uint32_t i) const { uint32_t shard = i >> (32 - SHARD_BITS); uint32_t offset = i & (~(~0u << (32 - SHARD_BITS))); @@ -83,8 +90,25 @@ const AttributePair& AttributePairStore::getPair(uint32_t i) const { return tlsHotShard[offset]; } + tlsPairLookups++; + if (tlsPairLookups % 1024 == 0) + lookups += 1024; + + const size_t candidateIndex = i % cachedAttributePairIndexes.size(); + if (cachedAttributePairIndexes[candidateIndex] == i) + return *cachedAttributePairPointers[candidateIndex]; + std::lock_guard lock(pairsMutex[shard]); - return pairs[shard][offset]; + + tlsPairLookupsUncached++; + if (tlsPairLookupsUncached % 1024 == 0) + lookupsUncached += 1024; + + const auto& rv = pairs[shard][offset]; + + cachedAttributePairIndexes[candidateIndex] = i; + cachedAttributePairPointers[candidateIndex] = &rv; + return rv; }; const AttributePair& AttributePairStore::getPairUnsafe(uint32_t i) const { @@ -268,8 +292,8 @@ void AttributeSet::finalize() { thread_local std::vector cachedAttributeSetPointers(64); thread_local std::vector cachedAttributeSetIndexes(64); -thread_local uint64_t tlsLookups = 0; -thread_local uint64_t tlsLookupsUncached = 0; +thread_local uint64_t tlsSetLookups = 0; +thread_local uint64_t tlsSetLookupsUncached = 0; AttributeIndex AttributeStore::add(AttributeSet &attributes) { // TODO: there's probably a way to use C++ types to distinguish a finalized // and non-finalized AttributeSet, which would make this safer. @@ -280,8 +304,8 @@ AttributeIndex AttributeStore::add(AttributeSet &attributes) { const size_t candidateIndex = hash % cachedAttributeSetPointers.size(); // Before taking a lock, see if we've seen this attribute set recently. - tlsLookups++; - if (tlsLookups % 1024 == 0) { + tlsSetLookups++; + if (tlsSetLookups % 1024 == 0) { lookups += 1024; } @@ -299,8 +323,8 @@ AttributeIndex AttributeStore::add(AttributeSet &attributes) { shard = shard >> 2; std::lock_guard lock(setsMutex[shard]); - tlsLookupsUncached++; - if (tlsLookupsUncached % 1024 == 0) + tlsSetLookupsUncached++; + if (tlsSetLookupsUncached % 1024 == 0) lookupsUncached += 1024; const uint32_t offset = sets[shard].add(attributes); @@ -346,7 +370,7 @@ size_t AttributeStore::size() const { } void AttributeStore::reportSize() const { - std::cout << "Attributes: " << size() << " sets from " << lookups.load() << " objects (" << lookupsUncached.load() << " uncached)" << std::endl; + std::cout << "Attributes: " << size() << " sets from " << lookups.load() << " objects (" << lookupsUncached.load() << " uncached), " << pairStore.lookups.load() << " pair lookups (" << pairStore.lookupsUncached.load() << " uncached)" << std::endl; // Print detailed histogram of frequencies of attributes. if (false) { From 58d49c8e1b87cbcc7006d3f056938b21ae12ce0d Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 25 Dec 2023 12:47:52 -0500 Subject: [PATCH 69/81] Revert "AttributePairStore.getPair: add thread-local cache" This reverts commit e8720738fe522a00f7d13b10339fc73955e7a66e. This didn't seem to be a win - less system time, but more overall CPU time. Let's fix the bigger contention issues, and consider revisiting this. In fact, AttributePairStore::getPair is only called by removePairWithKey. We could rejig OsmLuaProcessing to do this filtering prior to creating an AttributeSet - then there's no need for locks at all. --- include/attribute_store.h | 15 ++++----------- src/attribute_store.cpp | 40 ++++++++------------------------------- 2 files changed, 12 insertions(+), 43 deletions(-) diff --git a/include/attribute_store.h b/include/attribute_store.h index 98fda7c6..95c4cabb 100644 --- a/include/attribute_store.h +++ b/include/attribute_store.h @@ -183,15 +183,11 @@ struct AttributePair { #define SHARD_BITS 14 #define ATTRIBUTE_SHARDS (1 << SHARD_BITS) -class AttributeStore; - class AttributePairStore { public: AttributePairStore(): finalized(false), - pairsMutex(ATTRIBUTE_SHARDS), - lookups(0), - lookupsUncached(0) + pairsMutex(ATTRIBUTE_SHARDS) { // The "hot" shard has a capacity of 64K, the others are unbounded. pairs.push_back(DequeMap(1 << 16)); @@ -206,9 +202,9 @@ class AttributePairStore { const AttributePair& getPairUnsafe(uint32_t i) const; uint32_t addPair(AttributePair& pair, bool isHot); -private: - friend class AttributeStore; std::vector> pairs; + +private: bool finalized; // We refer to all attribute pairs by index. // @@ -218,8 +214,6 @@ class AttributePairStore { // we suspect will be popular. It only ever has 64KB items, // so that we can reference it with a short. mutable std::vector pairsMutex; - mutable std::atomic lookupsUncached; - mutable std::atomic lookups; }; // AttributeSet is a set of AttributePairs @@ -412,8 +406,7 @@ struct AttributeStore { finalized(false), sets(ATTRIBUTE_SHARDS), setsMutex(ATTRIBUTE_SHARDS), - lookups(0), - lookupsUncached(0) { + lookups(0) { } AttributeKeyStore keyStore; diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index e0519179..7ce784b8 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -67,13 +67,6 @@ void AttributePair::ensureStringIsOwned() { // AttributePairStore thread_local DequeMap tlsHotShard(1 << 16); -// Remember recently queried/added sets so that we can return them in the -// future without taking a lock. -thread_local std::vector cachedAttributePairPointers(64); -thread_local std::vector cachedAttributePairIndexes(64); - -thread_local uint64_t tlsPairLookups = 0; -thread_local uint64_t tlsPairLookupsUncached = 0; const AttributePair& AttributePairStore::getPair(uint32_t i) const { uint32_t shard = i >> (32 - SHARD_BITS); uint32_t offset = i & (~(~0u << (32 - SHARD_BITS))); @@ -90,25 +83,8 @@ const AttributePair& AttributePairStore::getPair(uint32_t i) const { return tlsHotShard[offset]; } - tlsPairLookups++; - if (tlsPairLookups % 1024 == 0) - lookups += 1024; - - const size_t candidateIndex = i % cachedAttributePairIndexes.size(); - if (cachedAttributePairIndexes[candidateIndex] == i) - return *cachedAttributePairPointers[candidateIndex]; - std::lock_guard lock(pairsMutex[shard]); - - tlsPairLookupsUncached++; - if (tlsPairLookupsUncached % 1024 == 0) - lookupsUncached += 1024; - - const auto& rv = pairs[shard][offset]; - - cachedAttributePairIndexes[candidateIndex] = i; - cachedAttributePairPointers[candidateIndex] = &rv; - return rv; + return pairs[shard][offset]; }; const AttributePair& AttributePairStore::getPairUnsafe(uint32_t i) const { @@ -292,8 +268,8 @@ void AttributeSet::finalize() { thread_local std::vector cachedAttributeSetPointers(64); thread_local std::vector cachedAttributeSetIndexes(64); -thread_local uint64_t tlsSetLookups = 0; -thread_local uint64_t tlsSetLookupsUncached = 0; +thread_local uint64_t tlsLookups = 0; +thread_local uint64_t tlsLookupsUncached = 0; AttributeIndex AttributeStore::add(AttributeSet &attributes) { // TODO: there's probably a way to use C++ types to distinguish a finalized // and non-finalized AttributeSet, which would make this safer. @@ -304,8 +280,8 @@ AttributeIndex AttributeStore::add(AttributeSet &attributes) { const size_t candidateIndex = hash % cachedAttributeSetPointers.size(); // Before taking a lock, see if we've seen this attribute set recently. - tlsSetLookups++; - if (tlsSetLookups % 1024 == 0) { + tlsLookups++; + if (tlsLookups % 1024 == 0) { lookups += 1024; } @@ -323,8 +299,8 @@ AttributeIndex AttributeStore::add(AttributeSet &attributes) { shard = shard >> 2; std::lock_guard lock(setsMutex[shard]); - tlsSetLookupsUncached++; - if (tlsSetLookupsUncached % 1024 == 0) + tlsLookupsUncached++; + if (tlsLookupsUncached % 1024 == 0) lookupsUncached += 1024; const uint32_t offset = sets[shard].add(attributes); @@ -370,7 +346,7 @@ size_t AttributeStore::size() const { } void AttributeStore::reportSize() const { - std::cout << "Attributes: " << size() << " sets from " << lookups.load() << " objects (" << lookupsUncached.load() << " uncached), " << pairStore.lookups.load() << " pair lookups (" << pairStore.lookupsUncached.load() << " uncached)" << std::endl; + std::cout << "Attributes: " << size() << " sets from " << lookups.load() << " objects (" << lookupsUncached.load() << " uncached)" << std::endl; // Print detailed histogram of frequencies of attributes. if (false) { From 1c16faeff71e3f2e644e6dba8e6ae3daa3985279 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 25 Dec 2023 14:17:29 -0500 Subject: [PATCH 70/81] move duplicate attribute handling outside of locks --- include/osm_lua_processing.h | 3 +++ src/attribute_store.cpp | 3 --- src/osm_lua_processing.cpp | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/include/osm_lua_processing.h b/include/osm_lua_processing.h index 1d513829..6a6a1d5d 100644 --- a/include/osm_lua_processing.h +++ b/include/osm_lua_processing.h @@ -225,6 +225,8 @@ class OsmLuaProcessing { lastStoredGeometryId = 0; } + void removeAttributeIfNeeded(const std::string& key); + const inline Point getPoint() { return Point(lon/10000000.0,latp/10000000.0); } @@ -267,6 +269,7 @@ class OsmLuaProcessing { class LayerDefinition &layers; std::vector> outputs; // All output objects that have been created + std::vector outputKeys; std::vector finalizeOutputs(); diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index 7ce784b8..3d9ad4e6 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -210,19 +210,16 @@ void AttributeStore::addAttribute(AttributeSet& attributeSet, std::string const PooledString ps(&v); AttributePair kv(keyStore.key2index(key), ps, minzoom); bool isHot = AttributePair::isHot(key, v); - attributeSet.removePairWithKey(pairStore, kv.keyIndex); attributeSet.addPair(pairStore.addPair(kv, isHot)); } void AttributeStore::addAttribute(AttributeSet& attributeSet, std::string const &key, bool v, char minzoom) { AttributePair kv(keyStore.key2index(key),v,minzoom); bool isHot = true; // All bools are eligible to be hot pairs - attributeSet.removePairWithKey(pairStore, kv.keyIndex); attributeSet.addPair(pairStore.addPair(kv, isHot)); } void AttributeStore::addAttribute(AttributeSet& attributeSet, std::string const &key, float v, char minzoom) { AttributePair kv(keyStore.key2index(key),v,minzoom); bool isHot = v >= 0 && v <= 25 && ceil(v) == v; // Whole numbers in 0..25 are eligible to be hot pairs - attributeSet.removePairWithKey(pairStore, kv.keyIndex); attributeSet.addPair(pairStore.addPair(kv, isHot)); } diff --git a/src/osm_lua_processing.cpp b/src/osm_lua_processing.cpp index 52dad9b6..31d184ed 100644 --- a/src/osm_lua_processing.cpp +++ b/src/osm_lua_processing.cpp @@ -442,6 +442,7 @@ const MultiPolygon &OsmLuaProcessing::multiPolygonCached() { // Add object to specified layer from Lua void OsmLuaProcessing::Layer(const string &layerName, bool area) { + outputKeys.clear(); if (layers.layerMap.count(layerName) == 0) { throw out_of_range("ERROR: Layer(): a layer named as \"" + layerName + "\" doesn't exist."); } @@ -558,6 +559,7 @@ void OsmLuaProcessing::Layer(const string &layerName, bool area) { } void OsmLuaProcessing::LayerAsCentroid(const string &layerName) { + outputKeys.clear(); if (layers.layerMap.count(layerName) == 0) { throw out_of_range("ERROR: LayerAsCentroid(): a layer named as \"" + layerName + "\" doesn't exist."); } @@ -629,6 +631,19 @@ void OsmLuaProcessing::Accept() { relationAccepted = true; } +void OsmLuaProcessing::removeAttributeIfNeeded(const string& key) { + // Does it exist? + for (int i = 0; i < outputKeys.size(); i++) { + if (outputKeys[i] == key) { + AttributeSet& set = outputs.back().second; + set.removePairWithKey(attributeStore.pairStore, attributeStore.keyStore.key2index(key)); + return; + } + } + + outputKeys.push_back(key); +} + // Set attributes in a vector tile's Attributes table void OsmLuaProcessing::AttributeWithMinZoom(const string &key, const PossiblyKnownTagValue& val, const char minzoom) { std::string str; @@ -642,18 +657,21 @@ void OsmLuaProcessing::AttributeWithMinZoom(const string &key, const PossiblyKno if (str.size()==0) { return; } // don't set empty strings if (outputs.size()==0) { ProcessingError("Can't add Attribute if no Layer set"); return; } + removeAttributeIfNeeded(key); attributeStore.addAttribute(outputs.back().second, key, str, minzoom); setVectorLayerMetadata(outputs.back().first.layer, key, 0); } void OsmLuaProcessing::AttributeNumericWithMinZoom(const string &key, const float val, const char minzoom) { if (outputs.size()==0) { ProcessingError("Can't add Attribute if no Layer set"); return; } + removeAttributeIfNeeded(key); attributeStore.addAttribute(outputs.back().second, key, val, minzoom); setVectorLayerMetadata(outputs.back().first.layer, key, 1); } void OsmLuaProcessing::AttributeBooleanWithMinZoom(const string &key, const bool val, const char minzoom) { if (outputs.size()==0) { ProcessingError("Can't add Attribute if no Layer set"); return; } + removeAttributeIfNeeded(key); attributeStore.addAttribute(outputs.back().second, key, val, minzoom); setVectorLayerMetadata(outputs.back().first.layer, key, 2); } From 0584403e9aad2be3616c398ecebf066e95f3cdf1 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 25 Dec 2023 16:01:30 -0500 Subject: [PATCH 71/81] add thread-local cache for attributepairs --- include/attribute_store.h | 13 ++++++--- src/attribute_store.cpp | 55 ++++++++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/include/attribute_store.h b/include/attribute_store.h index 95c4cabb..6f11ba00 100644 --- a/include/attribute_store.h +++ b/include/attribute_store.h @@ -183,11 +183,14 @@ struct AttributePair { #define SHARD_BITS 14 #define ATTRIBUTE_SHARDS (1 << SHARD_BITS) +class AttributeStore; class AttributePairStore { public: AttributePairStore(): finalized(false), - pairsMutex(ATTRIBUTE_SHARDS) + pairsMutex(ATTRIBUTE_SHARDS), + lookups(0), + lookupsUncached(0) { // The "hot" shard has a capacity of 64K, the others are unbounded. pairs.push_back(DequeMap(1 << 16)); @@ -202,9 +205,10 @@ class AttributePairStore { const AttributePair& getPairUnsafe(uint32_t i) const; uint32_t addPair(AttributePair& pair, bool isHot); - std::vector> pairs; private: + friend class AttributeStore; + std::vector> pairs; bool finalized; // We refer to all attribute pairs by index. // @@ -214,6 +218,8 @@ class AttributePairStore { // we suspect will be popular. It only ever has 64KB items, // so that we can reference it with a short. mutable std::vector pairsMutex; + std::atomic lookupsUncached; + std::atomic lookups; }; // AttributeSet is a set of AttributePairs @@ -406,7 +412,8 @@ struct AttributeStore { finalized(false), sets(ATTRIBUTE_SHARDS), setsMutex(ATTRIBUTE_SHARDS), - lookups(0) { + lookups(0), + lookupsUncached(0) { } AttributeKeyStore keyStore; diff --git a/src/attribute_store.cpp b/src/attribute_store.cpp index 3d9ad4e6..363d167b 100644 --- a/src/attribute_store.cpp +++ b/src/attribute_store.cpp @@ -97,6 +97,13 @@ const AttributePair& AttributePairStore::getPairUnsafe(uint32_t i) const { return pairs[shard][offset]; }; +// Remember recently queried/added pairs so that we can return them in the +// future without taking a lock. +thread_local uint64_t tlsPairLookups = 0; +thread_local uint64_t tlsPairLookupsUncached = 0; + +thread_local std::vector cachedAttributePairPointers(64); +thread_local std::vector cachedAttributePairIndexes(64); uint32_t AttributePairStore::addPair(AttributePair& pair, bool isHot) { if (isHot) { { @@ -132,6 +139,23 @@ uint32_t AttributePairStore::addPair(AttributePair& pair, bool isHot) { // Throw it on the pile with the rest of the pairs. size_t hash = pair.hash(); + const size_t candidateIndex = hash % cachedAttributePairPointers.size(); + // Before taking a lock, see if we've seen this attribute pair recently. + + tlsPairLookups++; + if (tlsPairLookups % 1024 == 0) { + lookups += 1024; + } + + + { + const AttributePair* candidate = cachedAttributePairPointers[candidateIndex]; + + if (candidate != nullptr && *candidate == pair) + return cachedAttributePairIndexes[candidateIndex]; + } + + size_t shard = hash % ATTRIBUTE_SHARDS; // Shard 0 is for hot pairs -- pick another shard if it gets selected. if (shard == 0) shard = (hash >> 8) % ATTRIBUTE_SHARDS; @@ -140,9 +164,19 @@ uint32_t AttributePairStore::addPair(AttributePair& pair, bool isHot) { if (shard == 0) shard = 1; std::lock_guard lock(pairsMutex[shard]); + + tlsPairLookupsUncached++; + if (tlsPairLookupsUncached % 1024 == 0) + lookupsUncached += 1024; + const auto& index = pairs[shard].find(pair); - if (index != -1) - return (shard << (32 - SHARD_BITS)) + index; + if (index != -1) { + const uint32_t rv = (shard << (32 - SHARD_BITS)) + index; + cachedAttributePairPointers[candidateIndex] = &pairs[shard][index]; + cachedAttributePairIndexes[candidateIndex] = rv; + + return rv; + } pair.ensureStringIsOwned(); uint32_t offset = pairs[shard].add(pair); @@ -265,8 +299,8 @@ void AttributeSet::finalize() { thread_local std::vector cachedAttributeSetPointers(64); thread_local std::vector cachedAttributeSetIndexes(64); -thread_local uint64_t tlsLookups = 0; -thread_local uint64_t tlsLookupsUncached = 0; +thread_local uint64_t tlsSetLookups = 0; +thread_local uint64_t tlsSetLookupsUncached = 0; AttributeIndex AttributeStore::add(AttributeSet &attributes) { // TODO: there's probably a way to use C++ types to distinguish a finalized // and non-finalized AttributeSet, which would make this safer. @@ -277,8 +311,8 @@ AttributeIndex AttributeStore::add(AttributeSet &attributes) { const size_t candidateIndex = hash % cachedAttributeSetPointers.size(); // Before taking a lock, see if we've seen this attribute set recently. - tlsLookups++; - if (tlsLookups % 1024 == 0) { + tlsSetLookups++; + if (tlsSetLookups % 1024 == 0) { lookups += 1024; } @@ -296,8 +330,8 @@ AttributeIndex AttributeStore::add(AttributeSet &attributes) { shard = shard >> 2; std::lock_guard lock(setsMutex[shard]); - tlsLookupsUncached++; - if (tlsLookupsUncached % 1024 == 0) + tlsSetLookupsUncached++; + if (tlsSetLookupsUncached % 1024 == 0) lookupsUncached += 1024; const uint32_t offset = sets[shard].add(attributes); @@ -343,7 +377,7 @@ size_t AttributeStore::size() const { } void AttributeStore::reportSize() const { - std::cout << "Attributes: " << size() << " sets from " << lookups.load() << " objects (" << lookupsUncached.load() << " uncached)" << std::endl; + std::cout << "Attributes: " << size() << " sets from " << lookups.load() << " objects (" << lookupsUncached.load() << " uncached), " << pairStore.lookups.load() << " pairs (" << pairStore.lookupsUncached.load() << " uncached)" << std::endl; // Print detailed histogram of frequencies of attributes. if (false) { @@ -409,6 +443,9 @@ void AttributeStore::reset() { for (int i = 0; i < cachedAttributeSetPointers.size(); i++) cachedAttributeSetPointers[i] = nullptr; + + for (int i = 0; i < cachedAttributePairPointers.size(); i++) + cachedAttributePairPointers[i] = nullptr; } void AttributeStore::finalize() { From fd969437cd5675c6de026a2080e5facf3d00aa36 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 25 Dec 2023 17:22:24 -0500 Subject: [PATCH 72/81] buffer objects when object index contended --- include/tile_data.h | 4 ++++ src/tile_data.cpp | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/include/tile_data.h b/include/tile_data.h index 6b59ee3f..b78463e2 100644 --- a/include/tile_data.h +++ b/include/tile_data.h @@ -364,6 +364,8 @@ class TileDataSource { ClipCache multiPolygonClipCache; ClipCache multiLinestringClipCache; + std::deque>> pendingSmallIndexObjects; + public: TileDataSource(size_t threadNum, unsigned int baseZoom, bool includeID); @@ -391,6 +393,8 @@ class TileDataSource { ); void addObjectToSmallIndex(const TileCoordinates& index, const OutputObject& oo, uint64_t id); + void addObjectToSmallIndex(const TileCoordinates& index, const OutputObject& oo, uint64_t id, bool needsLock); + void addObjectToSmallIndexUnsafe(const TileCoordinates& index, const OutputObject& oo, uint64_t id); void addObjectToLargeIndex(const Box& envelope, const OutputObject& oo, uint64_t id) { std::lock_guard lock(mutex); diff --git a/src/tile_data.cpp b/src/tile_data.cpp index f78bbdda..407f534a 100644 --- a/src/tile_data.cpp +++ b/src/tile_data.cpp @@ -73,10 +73,21 @@ TileDataSource::TileDataSource(size_t threadNum, unsigned int baseZoom, bool inc } } +thread_local std::vector>* tlsPendingSmallIndexObjects = nullptr; + void TileDataSource::finalize(size_t threadNum) { + uint64_t finalized = 0; + for (const auto& vec : pendingSmallIndexObjects) { + for (const auto& tuple : vec) { + finalized++; + addObjectToSmallIndexUnsafe(std::get<0>(tuple), std::get<1>(tuple), std::get<2>(tuple)); + } + } + + std::cout << "indexed " << finalized << " contended objects" << std::endl; + finalizeObjects(name(), threadNum, baseZoom, objects.begin(), objects.end(), lowZoomObjects); finalizeObjects(name(), threadNum, baseZoom, objectsWithIds.begin(), objectsWithIds.end(), lowZoomObjectsWithIds); - } void TileDataSource::addObjectToSmallIndex(const TileCoordinates& index, const OutputObject& oo, uint64_t id) { @@ -90,8 +101,28 @@ void TileDataSource::addObjectToSmallIndex(const TileCoordinates& index, const O } const size_t z6index = z6x * CLUSTER_ZOOM_WIDTH + z6y; + auto& mutex = objectsMutex[z6index % objectsMutex.size()]; + + if (mutex.try_lock()) { + addObjectToSmallIndexUnsafe(index, oo, id); + mutex.unlock(); + } else { + // add to tlsPendingSmallIndexObjects + if (tlsPendingSmallIndexObjects == nullptr) { + std::lock_guard lock(objectsMutex[0]); + pendingSmallIndexObjects.push_back(std::vector>()); + tlsPendingSmallIndexObjects = &pendingSmallIndexObjects.back(); + } - std::lock_guard lock(objectsMutex[z6index % objectsMutex.size()]); + tlsPendingSmallIndexObjects->push_back(std::make_tuple(index, oo, id)); + } +} + +void TileDataSource::addObjectToSmallIndexUnsafe(const TileCoordinates& index, const OutputObject& oo, uint64_t id) { + // Pick the z6 index + const size_t z6x = index.x / z6OffsetDivisor; + const size_t z6y = index.y / z6OffsetDivisor; + const size_t z6index = z6x * CLUSTER_ZOOM_WIDTH + z6y; if (id == 0 || !includeID) objects[z6index].push_back({ From 657da1ab92fcf65de3f5adafcceddc064ef5e73d Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Mon, 25 Dec 2023 23:02:08 -0500 Subject: [PATCH 73/81] --store uses lazy geometries; permit overriding I did some experiments on a Hetzner 48-core box with 192GB of RAM: --store, materialize geometries: real 65m34.327s user 2297m50.204s sys 65m0.901s The process often failed to use 100% of CPU--if you naively divide user+sys/real you get ~36, whereas the ideal would be ~48. Looking at stack traces, it seemed to coincide with calls to Boost's rbtree_best_fit allocator. Maybe: - we're doing disk I/O, and it's just slower than recomputing the geometries - we're using the Boost mmap library suboptimally -- maybe there's some other allocator we could be using. I think we use the mmap allocator like a simple bump allocator, so I don't know why we'd need a red-black tree --store, lazy geometries: real 55m33.979s user 2386m27.294s sys 23m58.973s Faster, but still some overhead (user+sys/real => ~43) no --store, materialize geometries: OOM no --store, lazy geometries (used 175GB): real 51m27.779s user 2306m25.309s sys 16m34.289s This was almost 100% CPU - user+sys/real => ~45) From this, I infer: - `--store` should always default to lazy geometries in order to minimize the I/O burden - `--materialize-geometries` is a good default for non-store usage, but it's still useful to be able to override and use lazy geometries, if it then means you can fit the data entirely in memory --- include/options_parser.h | 3 +++ src/options_parser.cpp | 13 +++++++++---- test/options_parser.test.cpp | 15 ++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/include/options_parser.h b/include/options_parser.h index c5307932..3ca73785 100644 --- a/include/options_parser.h +++ b/include/options_parser.h @@ -28,6 +28,9 @@ namespace OptionsParser { bool uncompressedNodes = false; bool uncompressedWays = false; bool materializeGeometries = false; + // lazyGeometries is the inverse of materializeGeometries. It can be passed + // to override an implicit materializeGeometries, as in the non-store case. + bool lazyGeometries = false; bool shardStores = false; }; diff --git a/src/options_parser.cpp b/src/options_parser.cpp index 3ea60798..529e5f4a 100644 --- a/src/options_parser.cpp +++ b/src/options_parser.cpp @@ -35,7 +35,8 @@ po::options_description getParser(OptionsParser::Options& options) { ("compact",po::bool_switch(&options.osm.compact), "use faster data structure for node lookups\nNOTE: This requires the input to be renumbered (osmium renumber)") ("no-compress-nodes", po::bool_switch(&options.osm.uncompressedNodes), "store nodes uncompressed") ("no-compress-ways", po::bool_switch(&options.osm.uncompressedWays), "store ways uncompressed") - ("materialize-geometries", po::bool_switch(&options.osm.materializeGeometries), "materialize geometries") + ("lazy-geometries", po::bool_switch(&options.osm.lazyGeometries), "generate geometries from the OSM stores; uses less memory") + ("materialize-geometries", po::bool_switch(&options.osm.materializeGeometries), "materialize geometries; uses more memory") ("shard-stores", po::bool_switch(&options.osm.shardStores), "use an alternate reading/writing strategy for low-memory machines") ("threads",po::value(&options.threadNum)->default_value(0), "number of threads (automatically detected if 0)") ; @@ -68,12 +69,16 @@ OptionsParser::Options OptionsParser::parse(const int argc, const char* argv[]) if (options.osm.storeFile.empty()) { options.osm.materializeGeometries = true; } else { - if (options.osm.fast) { - options.osm.materializeGeometries = true; - } else { + if (!options.osm.fast) { options.osm.shardStores = true; } } + + // You can pass --lazy-geometries to override the default of materialized geometries for + // the non-store case. + if (options.osm.lazyGeometries) + options.osm.materializeGeometries = false; + if (vm.count("help")) { options.showHelp = true; diff --git a/test/options_parser.test.cpp b/test/options_parser.test.cpp index 10e09597..e230fc0d 100644 --- a/test/options_parser.test.cpp +++ b/test/options_parser.test.cpp @@ -53,6 +53,19 @@ MU_TEST(test_options_parser) { mu_check(!opts.osm.shardStores); } + // --lazy-geometries overrides default + { + std::vector args = {"--output", "foo.mbtiles", "--input", "ontario.pbf", "--lazy-geometries"}; + auto opts = parse(args); + mu_check(opts.inputFiles.size() == 1); + mu_check(opts.inputFiles[0] == "ontario.pbf"); + mu_check(opts.outputFile == "foo.mbtiles"); + mu_check(opts.outputMode == OutputMode::MBTiles); + mu_check(!opts.osm.materializeGeometries); + mu_check(opts.osm.lazyGeometries); + mu_check(!opts.osm.shardStores); + } + // --store should optimize for reduced memory { std::vector args = {"--output", "foo.mbtiles", "--input", "ontario.pbf", "--store", "/tmp/store"}; @@ -75,7 +88,7 @@ MU_TEST(test_options_parser) { mu_check(opts.outputFile == "foo.pmtiles"); mu_check(opts.outputMode == OutputMode::PMTiles); mu_check(opts.osm.storeFile == "/tmp/store"); - mu_check(opts.osm.materializeGeometries); + mu_check(!opts.osm.materializeGeometries); mu_check(!opts.osm.shardStores); } From 916ff31e344f1ae7881bda128f24aba5e2c35c22 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Tue, 26 Dec 2023 00:10:37 -0500 Subject: [PATCH 74/81] fix mac/windows build? --- src/pbf_reader.cpp | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp index 25723807..b2d22a1f 100644 --- a/src/pbf_reader.cpp +++ b/src/pbf_reader.cpp @@ -4,8 +4,6 @@ #include "pbf_reader.h" #include "helpers.h" -using namespace PbfReader; - // Where read_pbf.cpp has higher-level routines that populate our structures, // pbf_reader.cpp has low-level tools that interact with the protobuf. // @@ -19,7 +17,7 @@ using namespace PbfReader; // If you want to persist the data beyond that, you must make a copy in memory // that you own. -BlobHeader PbfReader::PbfReader::readBlobHeader(std::istream& input) { +PbfReader::BlobHeader PbfReader::PbfReader::readBlobHeader(std::istream& input) { // See https://wiki.openstreetmap.org/wiki/PBF_Format#File_format unsigned int size; input.read((char*)&size, sizeof(size)); @@ -98,7 +96,7 @@ protozero::data_view PbfReader::PbfReader::readBlob(int32_t datasize, std::istre return { &blobStorage2[0], blobStorage2.size() }; } -HeaderBBox PbfReader::PbfReader::readHeaderBBox(protozero::data_view data) { +PbfReader::HeaderBBox PbfReader::PbfReader::readHeaderBBox(protozero::data_view data) { HeaderBBox box{0, 0, 0, 0}; protozero::pbf_message message{data}; @@ -124,7 +122,7 @@ HeaderBBox PbfReader::PbfReader::readHeaderBBox(protozero::data_view data) { return box; } -HeaderBlock PbfReader::PbfReader::readHeaderBlock(protozero::data_view data) { +PbfReader::HeaderBlock PbfReader::PbfReader::readHeaderBlock(protozero::data_view data) { HeaderBlock block{false}; protozero::pbf_message message{data}; @@ -307,8 +305,8 @@ void PbfReader::PrimitiveGroup::ensureData() { } } -DenseNodes& PrimitiveGroup::nodes() const { return denseNodes; }; -PrimitiveBlock::PrimitiveGroups& PrimitiveBlock::groups() { return groupsImpl; }; +PbfReader::DenseNodes& PbfReader::PrimitiveGroup::nodes() const { return denseNodes; }; +PbfReader::PrimitiveBlock::PrimitiveGroups& PbfReader::PrimitiveBlock::groups() { return groupsImpl; }; void PbfReader::DenseNodes::clear() { ids.clear(); @@ -339,17 +337,17 @@ PbfReader::DenseNodes::Node& PbfReader::DenseNodes::Iterator::operator*() { return node; } -bool DenseNodes::empty() { +bool PbfReader::DenseNodes::empty() { return ids.empty(); } -PbfReader::DenseNodes::Iterator DenseNodes::begin() { +PbfReader::DenseNodes::Iterator PbfReader::DenseNodes::begin() { auto it = Iterator {-1, Node{}, *this}; ++it; return it; } -PbfReader::DenseNodes::Iterator DenseNodes::end() { +PbfReader::DenseNodes::Iterator PbfReader::DenseNodes::end() { return Iterator {static_cast(ids.size()), Node{}, *this}; } @@ -363,7 +361,7 @@ void PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator::operator++() { (*groups)[offset].ensureData(); } } -PrimitiveGroup& PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator::operator*() { +PbfReader::PrimitiveGroup& PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator::operator*() { return (*groups)[offset]; } PbfReader::PrimitiveBlock::PrimitiveGroups::Iterator PbfReader::PrimitiveBlock::PrimitiveGroups::begin() { @@ -444,7 +442,7 @@ void PbfReader::Ways::Iterator::readWay(protozero::data_view data) { } } -Ways& PbfReader::PrimitiveGroup::ways() const { +PbfReader::Ways& PbfReader::PrimitiveGroup::ways() const { return internalWays; } bool PbfReader::Ways::Iterator::operator!=(Ways::Iterator& other) const { @@ -544,7 +542,7 @@ void PbfReader::Relations::Iterator::readRelation(protozero::data_view data) { } } -Relations& PbfReader::PrimitiveGroup::relations() const { +PbfReader::Relations& PbfReader::PrimitiveGroup::relations() const { return internalRelations; } bool PbfReader::Relations::Iterator::operator!=(Relations::Iterator& other) const { @@ -582,7 +580,7 @@ PbfReader::Relations::Iterator PbfReader::Relations::end() { return Relations::Iterator{protozero::pbf_message{nullptr, 0}, -1, relation}; } -HeaderBlock PbfReader::PbfReader::readHeaderFromFile(std::istream& input) { +PbfReader::HeaderBlock PbfReader::PbfReader::readHeaderFromFile(std::istream& input) { BlobHeader bh = readBlobHeader(input); protozero::data_view blob = readBlob(bh.datasize, input); HeaderBlock header = readHeaderBlock(blob); From 49014155643df7369093815205824b75ec5b87c2 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Tue, 26 Dec 2023 00:19:47 -0500 Subject: [PATCH 75/81] fixes for Windows build --- src/pbf_reader.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp index b2d22a1f..a648d757 100644 --- a/src/pbf_reader.cpp +++ b/src/pbf_reader.cpp @@ -48,7 +48,7 @@ PbfReader::BlobHeader PbfReader::PbfReader::readBlobHeader(std::istream& input) break; default: // ignore data for unknown tags to allow for future extensions - // std::cout << "BlobHeader: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + // std::cout << "BlobHeader: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; message.skip(); } } @@ -83,7 +83,7 @@ protozero::data_view PbfReader::PbfReader::readBlob(int32_t datasize, std::istre view = message.get_view(); break; default: - throw std::runtime_error("Blob: unknown tag: " + std::to_string(static_cast(message.tag()))); + throw std::runtime_error("Blob: unknown tag: " + std::to_string(static_cast(message.tag()))); } } @@ -115,7 +115,7 @@ PbfReader::HeaderBBox PbfReader::PbfReader::readHeaderBBox(protozero::data_view box.maxLat = message.get_sint64() / 1000000000.0; break; default: - throw std::runtime_error("HeaderBBox: unknown tag: " + std::to_string(static_cast(message.tag()))); + throw std::runtime_error("HeaderBBox: unknown tag: " + std::to_string(static_cast(message.tag()))); } } @@ -139,7 +139,7 @@ PbfReader::HeaderBlock PbfReader::PbfReader::readHeaderBlock(protozero::data_vie } default: // ignore data for unknown tags to allow for future extensions - //std::cout << "HeaderBlock: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + //std::cout << "HeaderBlock: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; message.skip(); } } @@ -155,7 +155,7 @@ void PbfReader::PbfReader::readStringTable(protozero::data_view data, std::vecto stringTable.push_back(message.get_view()); break; default: - throw std::runtime_error("StringTable: unknown tag: " + std::to_string(static_cast(message.tag()))); + throw std::runtime_error("StringTable: unknown tag: " + std::to_string(static_cast(message.tag()))); } } } @@ -183,7 +183,7 @@ PbfReader::PrimitiveBlock& PbfReader::PbfReader::readPrimitiveBlock(protozero::d } default: // ignore data for unknown tags to allow for future extensions - //std::cout << "HeaderBlock: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + //std::cout << "HeaderBlock: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; message.skip(); } } @@ -234,7 +234,7 @@ void PbfReader::DenseNodes::readDenseNodes(protozero::data_view data) { default: // ignore data for unknown tags to allow for future extensions - //std::cout << "HeaderBlock: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + //std::cout << "HeaderBlock: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; message.skip(); } } @@ -300,7 +300,7 @@ void PbfReader::PrimitiveGroup::ensureData() { internalType = PrimitiveGroupType::ChangeSet; break; default: - throw std::runtime_error("PrimitiveGroup: unknown tag: " + std::to_string(static_cast(message.tag()))); + throw std::runtime_error("PrimitiveGroup: unknown tag: " + std::to_string(static_cast(message.tag()))); } } } @@ -436,7 +436,7 @@ void PbfReader::Ways::Iterator::readWay(protozero::data_view data) { default: // ignore data for unknown tags to allow for future extensions - //std::cout << "Way: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + //std::cout << "Way: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; message.skip(); } } @@ -536,7 +536,7 @@ void PbfReader::Relations::Iterator::readRelation(protozero::data_view data) { default: // ignore data for unknown tags to allow for future extensions - //std::cout << "Way: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; + //std::cout << "Way: unknown tag: " << std::to_string(static_cast(message.tag())) << std::endl; message.skip(); } } From 6d491fc681e05cc06b5411123e78a81cdf0d6f4c Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Tue, 26 Dec 2023 00:20:25 -0500 Subject: [PATCH 76/81] fix mac build? --- src/pbf_reader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp index a648d757..d3b2c4f1 100644 --- a/src/pbf_reader.cpp +++ b/src/pbf_reader.cpp @@ -464,7 +464,7 @@ bool PbfReader::Ways::empty() { } PbfReader::Ways::Iterator PbfReader::Ways::begin() { if (pg->type() != PrimitiveGroupType::Way) - return Ways::Iterator{protozero::pbf_message{nullptr, 0}, -1, way}; + return Ways::Iterator{protozero::pbf_message{nullptr, 0ul}, -1, way}; protozero::pbf_message message{pg->getDataView()}; if (message.next()) { From decd18be2a907ef7612ba6a3c0e8e95254e595d9 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Tue, 26 Dec 2023 00:23:16 -0500 Subject: [PATCH 77/81] more macos fixes --- src/pbf_reader.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp index d3b2c4f1..89a6171e 100644 --- a/src/pbf_reader.cpp +++ b/src/pbf_reader.cpp @@ -477,7 +477,7 @@ PbfReader::Ways::Iterator PbfReader::Ways::begin() { return Ways::Iterator{message, -1, way}; } PbfReader::Ways::Iterator PbfReader::Ways::end() { - return Ways::Iterator{protozero::pbf_message{nullptr, 0}, -1, way}; + return Ways::Iterator{protozero::pbf_message{nullptr, 0ul}, -1, way}; } void PbfReader::Relations::Iterator::readRelation(protozero::data_view data) { @@ -564,7 +564,7 @@ bool PbfReader::Relations::empty() { } PbfReader::Relations::Iterator PbfReader::Relations::begin() { if (pg->type() != PrimitiveGroupType::Relation) - return Relations::Iterator{protozero::pbf_message{nullptr, 0}, -1, relation}; + return Relations::Iterator{protozero::pbf_message{nullptr, 0ul}, -1, relation}; protozero::pbf_message message{pg->getDataView()}; if (message.next()) { @@ -577,7 +577,7 @@ PbfReader::Relations::Iterator PbfReader::Relations::begin() { return Relations::Iterator{message, -1, relation}; } PbfReader::Relations::Iterator PbfReader::Relations::end() { - return Relations::Iterator{protozero::pbf_message{nullptr, 0}, -1, relation}; + return Relations::Iterator{protozero::pbf_message{nullptr, 0ul}, -1, relation}; } PbfReader::HeaderBlock PbfReader::PbfReader::readHeaderFromFile(std::istream& input) { From 867fd1f1165da79de275326037ba8213085659d1 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Tue, 26 Dec 2023 11:01:25 -0500 Subject: [PATCH 78/81] read_pbf -> pbf_processor --- CMakeLists.txt | 2 +- Makefile | 4 ++-- include/{read_pbf.h => pbf_processor.h} | 0 src/{read_pbf.cpp => pbf_processor.cpp} | 2 +- src/pbf_reader.cpp | 2 +- src/sorted_way_store.cpp | 2 +- src/tilemaker.cpp | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename include/{read_pbf.h => pbf_processor.h} (100%) rename src/{read_pbf.cpp => pbf_processor.cpp} (99%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a7b27a1..c2e36570 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,10 +98,10 @@ file(GLOB tilemaker_src_files src/osm_mem_tiles.cpp src/osm_store.cpp src/output_object.cpp + src/pbf_processor.cpp src/pbf_reader.cpp src/pmtiles.cpp src/pooled_string.cpp - src/read_pbf.cpp src/read_shp.cpp src/sharded_node_store.cpp src/sharded_way_store.cpp diff --git a/Makefile b/Makefile index 9fabef90..feffd0f5 100644 --- a/Makefile +++ b/Makefile @@ -110,10 +110,10 @@ tilemaker: \ src/osm_mem_tiles.o \ src/osm_store.o \ src/output_object.o \ + src/pbf_processor.o \ + src/pbf_reader.o \ src/pmtiles.o \ src/pooled_string.o \ - src/pbf_reader.o \ - src/read_pbf.o \ src/read_shp.o \ src/sharded_node_store.o \ src/sharded_way_store.o \ diff --git a/include/read_pbf.h b/include/pbf_processor.h similarity index 100% rename from include/read_pbf.h rename to include/pbf_processor.h diff --git a/src/read_pbf.cpp b/src/pbf_processor.cpp similarity index 99% rename from src/read_pbf.cpp rename to src/pbf_processor.cpp index 28acaaea..0cf0f1d9 100644 --- a/src/read_pbf.cpp +++ b/src/pbf_processor.cpp @@ -1,5 +1,5 @@ #include -#include "read_pbf.h" +#include "pbf_processor.h" #include "pbf_reader.h" #include diff --git a/src/pbf_reader.cpp b/src/pbf_reader.cpp index 89a6171e..ed400a49 100644 --- a/src/pbf_reader.cpp +++ b/src/pbf_reader.cpp @@ -4,7 +4,7 @@ #include "pbf_reader.h" #include "helpers.h" -// Where read_pbf.cpp has higher-level routines that populate our structures, +// Where pbf_processor.cpp has higher-level routines that populate our structures, // pbf_reader.cpp has low-level tools that interact with the protobuf. // // The lifetime of an object is only until someone calls a readXyz function at diff --git a/src/sorted_way_store.cpp b/src/sorted_way_store.cpp index 450a4bcc..302deab9 100644 --- a/src/sorted_way_store.cpp +++ b/src/sorted_way_store.cpp @@ -209,7 +209,7 @@ void SortedWayStore::insertLatpLons(std::vector &newWays } void SortedWayStore::insertNodes(const std::vector>>& newWays) { - // read_pbf can call with an empty array if the only ways it read were unable to + // pbf_processor can call with an empty array if the only ways it read were unable to // be processed due to missing nodes, so be robust against empty way vector. if (newWays.empty()) return; diff --git a/src/tilemaker.cpp b/src/tilemaker.cpp index d11b9a5d..3c3f55fe 100644 --- a/src/tilemaker.cpp +++ b/src/tilemaker.cpp @@ -50,7 +50,7 @@ #include "options_parser.h" #include "shared_data.h" -#include "read_pbf.h" +#include "pbf_processor.h" #include "read_shp.h" #include "tile_worker.h" #include "osm_mem_tiles.h" From 9c10fc73104063349299a941a81f38ce9fe03125 Mon Sep 17 00:00:00 2001 From: Colin Dellow Date: Wed, 27 Dec 2023 15:37:18 -0500 Subject: [PATCH 79/81] use vtzero for writing Switch from libprotobuf to vtzero, and fix a few places where we copied the geometries during writing. GB, lua-interop: real 1m51.572s user 26m45.201s sys 0m16.575s GB, this branch: real 1m43.007s user 25m0.658s sys 0m15.263s I haven't looked too closely at the output tiles yet, but they seem correct. Still todo: revive support for --merge --- CMakeLists.txt | 1 - Makefile | 3 +- include/output_object.h | 9 +- include/vtzero/builder.hpp | 1365 +++++++++++++++++++++ include/vtzero/builder_impl.hpp | 267 ++++ include/vtzero/encoded_property_value.hpp | 244 ++++ include/vtzero/exception.hpp | 134 ++ include/vtzero/feature.hpp | 315 +++++ include/vtzero/feature_builder_impl.hpp | 126 ++ include/vtzero/geometry.hpp | 444 +++++++ include/vtzero/index.hpp | 264 ++++ include/vtzero/layer.hpp | 512 ++++++++ include/vtzero/output.hpp | 64 + include/vtzero/property.hpp | 89 ++ include/vtzero/property_mapper.hpp | 103 ++ include/vtzero/property_value.hpp | 398 ++++++ include/vtzero/types.hpp | 433 +++++++ include/vtzero/vector_tile.hpp | 290 +++++ include/vtzero/version.hpp | 36 + include/write_geometry.h | 46 - src/output_object.cpp | 24 + src/tile_worker.cpp | 247 +++- src/write_geometry.cpp | 159 --- 23 files changed, 5324 insertions(+), 249 deletions(-) create mode 100644 include/vtzero/builder.hpp create mode 100644 include/vtzero/builder_impl.hpp create mode 100644 include/vtzero/encoded_property_value.hpp create mode 100644 include/vtzero/exception.hpp create mode 100644 include/vtzero/feature.hpp create mode 100644 include/vtzero/feature_builder_impl.hpp create mode 100644 include/vtzero/geometry.hpp create mode 100644 include/vtzero/index.hpp create mode 100644 include/vtzero/layer.hpp create mode 100644 include/vtzero/output.hpp create mode 100644 include/vtzero/property.hpp create mode 100644 include/vtzero/property_mapper.hpp create mode 100644 include/vtzero/property_value.hpp create mode 100644 include/vtzero/types.hpp create mode 100644 include/vtzero/vector_tile.hpp create mode 100644 include/vtzero/version.hpp delete mode 100644 include/write_geometry.h delete mode 100644 src/write_geometry.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 803fb140..9faba768 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,7 +114,6 @@ file(GLOB tilemaker_src_files src/tilemaker.cpp src/tile_worker.cpp src/way_stores.cpp - src/write_geometry.cpp ) add_executable(tilemaker vector_tile.pb.cc ${tilemaker_src_files}) target_include_directories(tilemaker PRIVATE include) diff --git a/Makefile b/Makefile index 1ac184f1..28f7128e 100644 --- a/Makefile +++ b/Makefile @@ -125,8 +125,7 @@ tilemaker: \ src/tile_data.o \ src/tilemaker.o \ src/tile_worker.o \ - src/way_stores.o \ - src/write_geometry.o + src/way_stores.o $(CXX) $(CXXFLAGS) -o tilemaker $^ $(INC) $(LIB) $(LDFLAGS) test: \ diff --git a/include/output_object.h b/include/output_object.h index 9afd5cba..38d76ba9 100644 --- a/include/output_object.h +++ b/include/output_object.h @@ -10,6 +10,7 @@ #include "coordinates.h" #include "attribute_store.h" #include "osm_store.h" +#include // Protobuf #include "vector_tile.pb.h" @@ -78,7 +79,13 @@ class OutputObject { std::vector *valueList, AttributeStore const &attributeStore, vector_tile::Tile_Feature *featurePtr, char zoom) const; - + + void writeAttributes( + const AttributeStore& attributeStore, + vtzero::feature_builder& fbuilder, + char zoom + ) const; + /** * \brief Find a value in the value dictionary * (we can't easily use find() because of the different value-type encoding - diff --git a/include/vtzero/builder.hpp b/include/vtzero/builder.hpp new file mode 100644 index 00000000..781758e8 --- /dev/null +++ b/include/vtzero/builder.hpp @@ -0,0 +1,1365 @@ +#ifndef VTZERO_BUILDER_HPP +#define VTZERO_BUILDER_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file builder.hpp + * + * @brief Contains the classes and functions to build vector tiles. + */ + +#include "builder_impl.hpp" +#include "feature_builder_impl.hpp" +#include "geometry.hpp" +#include "types.hpp" +#include "vector_tile.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace vtzero { + + /** + * Used to build vector tiles. Whenever you are building a new vector + * tile, start with an object of this class and add layers. After all + * the data is added, call serialize(). + * + * @code + * layer some_existing_layer = ...; + * + * tile_builder builder; + * layer_builder layer_roads{builder, "roads"}; + * builder.add_existing_layer(some_existing_layer); + * ... + * std::string data = builder.serialize(); + * @endcode + */ + class tile_builder { + + friend class layer_builder; + + std::vector> m_layers; + + /** + * Add a new layer to the vector tile based on an existing layer. The + * new layer will have the same name, version, and extent as the + * existing layer. The new layer will not contain any features. This + * method is handy when copying some (but not all) data from an + * existing layer. + */ + detail::layer_builder_impl* add_layer(const layer& layer) { + auto* ptr = new detail::layer_builder_impl{layer.name(), layer.version(), layer.extent()}; + m_layers.emplace_back(ptr); + return ptr; + } + + /** + * Add a new layer to the vector tile with the specified name, version, + * and extent. + * + * @tparam TString Some string type (const char*, std::string, + * vtzero::data_view) or something that converts to one of + * these types. + * @param name Name of this layer. + * @param version Version of this layer (only version 1 and 2 are + * supported) + * @param extent Extent used for this layer. + */ + template + detail::layer_builder_impl* add_layer(TString&& name, uint32_t version, uint32_t extent) { + auto* ptr = new detail::layer_builder_impl{std::forward(name), version, extent}; + m_layers.emplace_back(ptr); + return ptr; + } + + public: + + /// Constructor + tile_builder() = default; + + /// Destructor + ~tile_builder() noexcept = default; + + /// Tile builders can not be copied. + tile_builder(const tile_builder&) = delete; + + /// Tile builders can not be copied. + tile_builder& operator=(const tile_builder&) = delete; + + /// Tile builders can be moved. + tile_builder(tile_builder&&) = default; + + /// Tile builders can be moved. + tile_builder& operator=(tile_builder&&) = default; + + /** + * Add an existing layer to the vector tile. The layer data will be + * copied over into the new vector_tile when the serialize() method + * is called. Until then, the data referenced here must stay available. + * + * @param data Reference to some data that must be a valid encoded + * layer. + */ + void add_existing_layer(data_view&& data) { + m_layers.emplace_back(new detail::layer_builder_impl{std::forward(data)}); + } + + /** + * Add an existing layer to the vector tile. The layer data will be + * copied over into the new vector_tile when the serialize() method + * is called. Until then, the data referenced here must stay available. + * + * @param layer Reference to the layer to be copied. + */ + void add_existing_layer(const layer& layer) { + add_existing_layer(layer.data()); + } + + /** + * Serialize the data accumulated in this builder into a vector tile. + * The data will be appended to the specified buffer. The buffer + * doesn't have to be empty. + * + * @tparam TBuffer Type of buffer. Must be std:string or other buffer + * type supported by protozero. + * @param buffer Buffer to append the encoded vector tile to. + */ + template + void serialize(TBuffer& buffer) const { + const std::size_t estimated_size = std::accumulate(m_layers.cbegin(), m_layers.cend(), 0ULL, [](std::size_t sum, const std::unique_ptr& layer) { + return sum + layer->estimated_size(); + }); + + protozero::basic_pbf_builder pbf_tile_builder{buffer}; + pbf_tile_builder.reserve(estimated_size); + for (const auto& layer : m_layers) { + layer->build(pbf_tile_builder); + } + } + + /** + * Serialize the data accumulated in this builder into a vector_tile + * and return it. + * + * If you want to use an existing buffer instead, use the serialize() + * member function taking a TBuffer& as parameter. + * + * @returns std::string Buffer with encoded vector_tile data. + */ + std::string serialize() const { + std::string data; + serialize(data); + return data; + } + + }; // class tile_builder + + /** + * The layer_builder is used to add a new layer to a vector tile that is + * being built. + */ + class layer_builder { + + vtzero::detail::layer_builder_impl* m_layer; + + friend class geometry_feature_builder; + friend class point_feature_builder; + friend class linestring_feature_builder; + friend class polygon_feature_builder; + + vtzero::detail::layer_builder_impl& get_layer_impl() noexcept { + return *m_layer; + } + + template + using is_layer = std::is_same::type>::type, layer>; + + public: + + /** + * Construct a layer_builder to build a new layer with the same name, + * version, and extent as an existing layer. + * + * @param tile The tile builder we want to create this layer in. + * @param layer Existing layer we want to use the name, version, and + * extent from + */ + layer_builder(vtzero::tile_builder& tile, const layer& layer) : + m_layer(tile.add_layer(layer)) { + } + + /** + * Construct a layer_builder to build a completely new layer. + * + * @tparam TString Some string type (such as std::string or const char*) + * @param tile The tile builder we want to create this layer in. + * @param name The name of the new layer. + * @param version The vector tile spec version of the new layer. + * @param extent The extent of the new layer. + */ + template ::value, int>::type = 0> + layer_builder(vtzero::tile_builder& tile, TString&& name, uint32_t version = 2, uint32_t extent = 4096) : + m_layer(tile.add_layer(std::forward(name), version, extent)) { + } + + /** + * Add key to the keys table without checking for duplicates. This + * function is usually used when an external index is used which takes + * care of the duplication check. + * + * @param text The key. + * @returns The index value of this key. + */ + index_value add_key_without_dup_check(const data_view text) { + return m_layer->add_key_without_dup_check(text); + } + + /** + * Add key to the keys table. This function will consult the internal + * index in the layer to make sure the key is only in the table once. + * It will either return the index value of an existing key or add the + * new key and return its index value. + * + * @param text The key. + * @returns The index value of this key. + */ + index_value add_key(const data_view text) { + return m_layer->add_key(text); + } + + /** + * Add value to the values table without checking for duplicates. This + * function is usually used when an external index is used which takes + * care of the duplication check. + * + * @param value The property value. + * @returns The index value of this value. + */ + index_value add_value_without_dup_check(const property_value value) { + return m_layer->add_value_without_dup_check(value); + } + + /** + * Add value to the values table without checking for duplicates. This + * function is usually used when an external index is used which takes + * care of the duplication check. + * + * @param value The property value. + * @returns The index value of this value. + */ + index_value add_value_without_dup_check(const encoded_property_value& value) { + return m_layer->add_value_without_dup_check(value); + } + + /** + * Add value to the values table. This function will consult the + * internal index in the layer to make sure the value is only in the + * table once. It will either return the index value of an existing + * value or add the new value and return its index value. + * + * @param value The property value. + * @returns The index value of this value. + */ + index_value add_value(const property_value value) { + return m_layer->add_value(value); + } + + /** + * Add value to the values table. This function will consult the + * internal index in the layer to make sure the value is only in the + * table once. It will either return the index value of an existing + * value or add the new value and return its index value. + * + * @param value The property value. + * @returns The index value of this value. + */ + index_value add_value(const encoded_property_value& value) { + return m_layer->add_value(value); + } + + /** + * Add a feature from an existing layer to the new layer. The feature + * will be copied completely over to the new layer including its + * geometry and all its properties. + */ + void add_feature(const feature& feature); + + }; // class layer_builder + + /** + * Parent class for the point_feature_builder, linestring_feature_builder + * and polygon_feature_builder classes. You can not instantiate this class + * directly, use it through its derived classes. + */ + class feature_builder : public detail::feature_builder_base { + + class countdown_value { + + uint32_t m_value = 0; + + public: + + countdown_value() noexcept = default; + + ~countdown_value() noexcept { + assert_is_zero(); + } + + countdown_value(const countdown_value&) = delete; + + countdown_value& operator=(const countdown_value&) = delete; + + countdown_value(countdown_value&& other) noexcept : + m_value(other.m_value) { + other.m_value = 0; + } + + countdown_value& operator=(countdown_value&& other) noexcept { + m_value = other.m_value; + other.m_value = 0; + return *this; + } + + uint32_t value() const noexcept { + return m_value; + } + + void set(const uint32_t value) noexcept { + m_value = value; + } + + void decrement() { + vtzero_assert(m_value > 0 && "too many calls to set_point()"); + --m_value; + } + + void assert_is_zero() const noexcept { + vtzero_assert_in_noexcept_function(m_value == 0 && + "not enough calls to set_point()"); + } + + }; // countdown_value + + protected: + + /// Encoded geometry. + protozero::packed_field_uint32 m_pbf_geometry{}; + + /// Number of points still to be set for the geometry to be complete. + countdown_value m_num_points; + + /// Last point (used to calculate delta between coordinates) + point m_cursor{0, 0}; + + /// Constructor. + explicit feature_builder(detail::layer_builder_impl* layer) : + feature_builder_base(layer) { + } + + /// Helper function to check size isn't too large + template + uint32_t check_num_points(T size) { + if (size >= (1UL << 29U)) { + throw geometry_exception{"Maximum of 2^29 - 1 points allowed in geometry"}; + } + return static_cast(size); + } + + /// Helper function to make sure we have everything before adding a property + void prepare_to_add_property() { + if (m_pbf_geometry.valid()) { + m_num_points.assert_is_zero(); + m_pbf_geometry.commit(); + } + if (!m_pbf_tags.valid()) { + m_pbf_tags = {m_feature_writer, detail::pbf_feature::tags}; + } + } + + public: + + /** + * If the feature was not committed, the destructor will roll back all + * the changes. + */ + ~feature_builder() { + try { + rollback(); + } catch (...) { + // ignore exceptions + } + } + + /// Builder classes can not be copied + feature_builder(const feature_builder&) = delete; + + /// Builder classes can not be copied + feature_builder& operator=(const feature_builder&) = delete; + + /// Builder classes can be moved + feature_builder(feature_builder&& other) noexcept = default; + + /// Builder classes can be moved + feature_builder& operator=(feature_builder&& other) noexcept = default; + + /** + * Set the ID of this feature. + * + * You can only call this method once and it must be before calling + * any method manipulating the geometry. + * + * @param id The ID. + */ + void set_id(uint64_t id) { + vtzero_assert(m_feature_writer.valid() && + "Can not call set_id() after commit() or rollback()"); + vtzero_assert(!m_pbf_geometry.valid() && + !m_pbf_tags.valid() && + "Call set_id() before setting the geometry or adding properties"); + set_id_impl(id); + } + + /** + * Copy the ID of an existing feature to this feature. If the + * feature doesn't have an ID, no ID is set. + * + * You can only call this method once and it must be before calling + * any method manipulating the geometry. + * + * @param feature The feature to copy the ID from. + */ + void copy_id(const feature& feature) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_id() after commit() or rollback()"); + vtzero_assert(!m_pbf_geometry.valid() && + !m_pbf_tags.valid() && + "Call copy_id() before setting the geometry or adding properties"); + if (feature.has_id()) { + set_id_impl(feature.id()); + } + } + + /** + * Add a property to this feature. Can only be called after all the + * methods manipulating the geometry. + * + * @tparam TProp Can be type index_value_pair or property. + * @param prop The property to add. + */ + template + void add_property(TProp&& prop) { + vtzero_assert(m_feature_writer.valid() && + "Can not call add_property() after commit() or rollback()"); + prepare_to_add_property(); + add_property_impl(std::forward(prop)); + } + + /** + * Copy all properties of an existing feature to the one being built. + * + * @param feature The feature to copy the properties from. + */ + void copy_properties(const feature& feature) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_properties() after commit() or rollback()"); + prepare_to_add_property(); + feature.for_each_property([this](const property& prop) { + add_property_impl(prop); + return true; + }); + } + + /** + * Copy all properties of an existing feature to the one being built + * using a property_mapper. + * + * @tparam TMapper Must be the property_mapper class or something + * equivalent. + * @param feature The feature to copy the properties from. + * @param mapper Instance of the property_mapper class. + */ + template + void copy_properties(const feature& feature, TMapper& mapper) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_properties() after commit() or rollback()"); + prepare_to_add_property(); + feature.for_each_property_indexes([this, &mapper](const index_value_pair& idxs) { + add_property_impl(mapper(idxs)); + return true; + }); + } + + /** + * Add a property to this feature. Can only be called after all the + * methods manipulating the geometry. + * + * @tparam TKey Can be type index_value or data_view or anything that + * converts to it. + * @tparam TValue Can be type index_value or property_value or + * encoded_property or anything that converts to it. + * @param key The key. + * @param value The value. + */ + template + void add_property(TKey&& key, TValue&& value) { + vtzero_assert(m_feature_writer.valid() && + "Can not call add_property() after commit() or rollback()"); + prepare_to_add_property(); + add_property_impl(std::forward(key), std::forward(value)); + } + + /** + * Commit this feature. Call this after all the details of this + * feature have been added. If this is not called, the feature + * will be rolled back when the destructor of the feature_builder is + * called. + * + * Once a feature has been committed or rolled back, further calls + * to commit() or rollback() don't do anything. + */ + void commit() { + if (m_feature_writer.valid()) { + vtzero_assert((m_pbf_geometry.valid() || m_pbf_tags.valid()) && + "Can not call commit before geometry was added"); + if (m_pbf_geometry.valid()) { + m_pbf_geometry.commit(); + } + do_commit(); + } + } + + /** + * Rollback this feature. Removed all traces of this feature from + * the layer_builder. Useful when you started creating a feature + * but then find out that its geometry is invalid or something like + * it. This will also happen automatically when the feature_builder + * is destructed and commit() hasn't been called on it. + * + * Once a feature has been committed or rolled back, further calls + * to commit() or rollback() don't do anything. + */ + void rollback() { + if (m_feature_writer.valid()) { + if (m_pbf_geometry.valid()) { + m_pbf_geometry.rollback(); + } + do_rollback(); + } + } + + }; // class feature_builder + + /** + * Used for adding a feature with a point geometry to a layer. After + * creating an object of this class you can add data to the feature in a + * specific order: + * + * * Optionally add the ID using set_id(). + * * Add the (multi)point geometry using add_point(), add_points() and + * set_point(), or add_points_from_container(). + * * Optionally add any number of properties using add_property(). + * + * @code + * vtzero::tile_builder tb; + * vtzero::layer_builder lb{tb}; + * vtzero::point_feature_builder fb{lb}; + * fb.set_id(123); // optionally set ID + * fb.add_point(10, 20) // add point geometry + * fb.add_property("foo", "bar"); // add property + * @endcode + */ + class point_feature_builder : public feature_builder { + + public: + + /** + * Constructor + * + * @param layer The layer we want to create this feature in. + */ + explicit point_feature_builder(layer_builder layer) : + feature_builder(&layer.get_layer_impl()) { + m_feature_writer.add_enum(detail::pbf_feature::type, static_cast(GeomType::POINT)); + } + + /** + * Add a single point as the geometry to this feature. + * + * @param p The point to add. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_point(const point p) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_geometry.valid() && + !m_pbf_tags.valid() && + "add_point() can only be called once"); + m_pbf_geometry = {m_feature_writer, detail::pbf_feature::geometry}; + m_pbf_geometry.add_element(detail::command_move_to(1)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y)); + } + + /** + * Add a single point as the geometry to this feature. + * + * @param x X coordinate of the point to add. + * @param y Y coordinate of the point to add. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_point(const int32_t x, const int32_t y) { + add_point(point{x, y}); + } + + /** + * Add a single point as the geometry to this feature. + * + * @tparam TPoint A type that can be converted to vtzero::point using + * the create_vtzero_point function. + * @param p The point to add. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void add_point(TPoint&& p) { + add_point(create_vtzero_point(std::forward(p))); + } + + /** + * Declare the intent to add a multipoint geometry with *count* points + * to this feature. + * + * @param count The number of points in the multipoint geometry. + * + * @pre @code count > 0 && count < 2^29 @endcode + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_points(uint32_t count) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_geometry.valid() && + "can not call add_points() twice or mix with add_point()"); + vtzero_assert(!m_pbf_tags.valid() && + "add_points() has to be called before properties are added"); + vtzero_assert(count > 0 && count < (1UL << 29U) && "add_points() must be called with 0 < count < 2^29"); + m_num_points.set(count); + m_pbf_geometry = {m_feature_writer, detail::pbf_feature::geometry}; + m_pbf_geometry.add_element(detail::command_move_to(count)); + } + + /** + * Set a point in the multipoint geometry. + * + * @param p The point. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_points(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const point p) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(m_pbf_geometry.valid() && + "call add_points() before set_point()"); + vtzero_assert(!m_pbf_tags.valid() && + "set_point() has to be called before properties are added"); + m_num_points.decrement(); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + m_cursor = p; + } + + /** + * Set a point in the multipoint geometry. + * + * @param x X coordinate of the point to set. + * @param y Y coordinate of the point to set. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_points(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const int32_t x, const int32_t y) { + set_point(point{x, y}); + } + + /** + * Set a point in the multipoint geometry. + * + * @tparam TPoint A type that can be converted to vtzero::point using + * the create_vtzero_point function. + * @param p The point to add. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_points(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void set_point(TPoint&& p) { + set_point(create_vtzero_point(std::forward(p))); + } + + /** + * Add the points from the specified container as multipoint geometry + * to this feature. + * + * @tparam TContainer The container type. Must support the size() + * method, be iterable using a range for loop, and contain + * objects of type vtzero::point or something convertible to + * it. + * @param container The container to read the points from. + * + * @throws geometry_exception If there are more than 2^32-1 members in + * the container. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void add_points_from_container(const TContainer& container) { + add_points(check_num_points(container.size())); + for (const auto& element : container) { + set_point(element); + } + } + + }; // class point_feature_builder + + /** + * Used for adding a feature with a (multi)linestring geometry to a layer. + * After creating an object of this class you can add data to the + * feature in a specific order: + * + * * Optionally add the ID using set_id(). + * * Add the (multi)linestring geometry using add_linestring() or + * add_linestring_from_container(). + * * Optionally add any number of properties using add_property(). + * + * @code + * vtzero::tile_builder tb; + * vtzero::layer_builder lb{tb}; + * vtzero::linestring_feature_builder fb{lb}; + * fb.set_id(123); // optionally set ID + * fb.add_linestring(2); + * fb.set_point(10, 10); + * fb.set_point(10, 20); + * fb.add_property("foo", "bar"); // add property + * @endcode + */ + class linestring_feature_builder : public feature_builder { + + bool m_start_line = false; + + public: + + /** + * Constructor + * + * @param layer The layer we want to create this feature in. + */ + explicit linestring_feature_builder(layer_builder layer) : + feature_builder(&layer.get_layer_impl()) { + m_feature_writer.add_enum(detail::pbf_feature::type, static_cast(GeomType::LINESTRING)); + } + + /** + * Declare the intent to add a linestring geometry with *count* points + * to this feature. + * + * @param count The number of points in the linestring. + * + * @pre @code count > 1 && count < 2^29 @endcode + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_linestring(const uint32_t count) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid() && + "add_linestring() has to be called before properties are added"); + vtzero_assert(count > 1 && count < (1UL << 29U) && "add_linestring() must be called with 1 < count < 2^29"); + m_num_points.assert_is_zero(); + if (!m_pbf_geometry.valid()) { + m_pbf_geometry = {m_feature_writer, detail::pbf_feature::geometry}; + } + m_num_points.set(count); + m_start_line = true; + } + + /** + * Set a point in the multilinestring geometry opened with + * add_linestring(). + * + * @param p The point. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_linestring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const point p) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(m_pbf_geometry.valid() && + "call add_linestring() before set_point()"); + vtzero_assert(!m_pbf_tags.valid() && + "set_point() has to be called before properties are added"); + m_num_points.decrement(); + if (m_start_line) { + m_pbf_geometry.add_element(detail::command_move_to(1)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + m_pbf_geometry.add_element(detail::command_line_to(m_num_points.value())); + m_start_line = false; + } else { + if (p == m_cursor) { + throw geometry_exception{"Zero-length segments in linestrings are not allowed."}; + } + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + } + m_cursor = p; + } + + /** + * Set a point in the multilinestring geometry opened with + * add_linestring(). + * + * @param x X coordinate of the point to set. + * @param y Y coordinate of the point to set. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_linestring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const int32_t x, const int32_t y) { + set_point(point{x, y}); + } + + /** + * Set a point in the multilinestring geometry opened with + * add_linestring(). + * + * @tparam TPoint A type that can be converted to vtzero::point using + * the create_vtzero_point function. + * @param p The point to add. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_linestring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void set_point(TPoint&& p) { + set_point(create_vtzero_point(std::forward(p))); + } + + /** + * Add the points from the specified container as a linestring geometry + * to this feature. + * + * @tparam TContainer The container type. Must support the size() + * method, be iterable using a range for loop, and contain + * objects of type vtzero::point or something convertible to + * it. + * @param container The container to read the points from. + * + * @throws geometry_exception If there are more than 2^32-1 members in + * the container or if two consecutive points in the container + * are identical. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void add_linestring_from_container(const TContainer& container) { + add_linestring(check_num_points(container.size())); + for (const auto& element : container) { + set_point(element); + } + } + + }; // class linestring_feature_builder + + /** + * Used for adding a feature with a (multi)polygon geometry to a layer. + * After creating an object of this class you can add data to the + * feature in a specific order: + * + * * Optionally add the ID using set_id(). + * * Add the (multi)polygon geometry using add_ring() or + * add_ring_from_container(). + * * Optionally add any number of properties using add_property(). + * + * @code + * vtzero::tile_builder tb; + * vtzero::layer_builder lb{tb}; + * vtzero::polygon_feature_builder fb{lb}; + * fb.set_id(123); // optionally set ID + * fb.add_ring(5); + * fb.set_point(10, 10); + * ... + * fb.add_property("foo", "bar"); // add property + * @endcode + */ + class polygon_feature_builder : public feature_builder { + + point m_first_point{0, 0}; + bool m_start_ring = false; + + public: + + /** + * Constructor + * + * @param layer The layer we want to create this feature in. + */ + explicit polygon_feature_builder(layer_builder layer) : + feature_builder(&layer.get_layer_impl()) { + m_feature_writer.add_enum(detail::pbf_feature::type, static_cast(GeomType::POLYGON)); + } + + /** + * Declare the intent to add a ring with *count* points to this + * feature. + * + * @param count The number of points in the ring. + * + * @pre @code count > 3 && count < 2^29 @endcode + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void add_ring(const uint32_t count) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid() && + "add_ring() has to be called before properties are added"); + vtzero_assert(count > 3 && count < (1UL << 29U) && "add_ring() must be called with 3 < count < 2^29"); + m_num_points.assert_is_zero(); + if (!m_pbf_geometry.valid()) { + m_pbf_geometry = {m_feature_writer, detail::pbf_feature::geometry}; + } + m_num_points.set(count); + m_start_ring = true; + } + + /** + * Set a point in the ring opened with add_ring(). + * + * @param p The point. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * This exception is also thrown when the last point in the + * ring is not equal to the first point, because this would + * not create a closed ring. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_ring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const point p) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(m_pbf_geometry.valid() && + "call add_ring() before set_point()"); + vtzero_assert(!m_pbf_tags.valid() && + "set_point() has to be called before properties are added"); + m_num_points.decrement(); + if (m_start_ring) { + m_first_point = p; + m_pbf_geometry.add_element(detail::command_move_to(1)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + m_pbf_geometry.add_element(detail::command_line_to(m_num_points.value() - 1)); + m_start_ring = false; + m_cursor = p; + } else if (m_num_points.value() == 0) { + if (p != m_first_point) { + throw geometry_exception{"Last point in a ring must be the same as the first point."}; + } + // spec 4.3.3.3 "A ClosePath command MUST have a command count of 1" + m_pbf_geometry.add_element(detail::command_close_path()); + } else { + if (p == m_cursor) { + throw geometry_exception{"Zero-length segments in rings are not allowed."}; + } + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.x - m_cursor.x)); + m_pbf_geometry.add_element(protozero::encode_zigzag32(p.y - m_cursor.y)); + m_cursor = p; + } + } + + /** + * Set a point in the ring opened with add_ring(). + * + * @param x X coordinate of the point to set. + * @param y Y coordinate of the point to set. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * This exception is also thrown when the last point in the + * ring is not equal to the first point, because this would + * not create a closed ring. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_ring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void set_point(const int32_t x, const int32_t y) { + set_point(point{x, y}); + } + + /** + * Set a point in the ring opened with add_ring(). + * + * @tparam TPoint A type that can be converted to vtzero::point using + * the create_vtzero_point function. + * @param p The point to add. + * + * @throws geometry_exception if the point set is the same as the + * previous point. This would create zero-length segments + * which are not allowed according to the vector tile spec. + * This exception is also thrown when the last point in the + * ring is not equal to the first point, because this would + * not create a closed ring. + * + * @pre There must have been less than *count* calls to set_point() + * already after a call to add_ring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void set_point(TPoint&& p) { + set_point(create_vtzero_point(std::forward(p))); + } + + /** + * Close a ring opened with add_ring(). This can be called for the + * last point (which will be equal to the first point) in the ring + * instead of calling set_point(). + * + * @pre There must have been *count* - 1 calls to set_point() + * already after a call to add_ring(count). + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + void close_ring() { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(m_pbf_geometry.valid() && + "Call add_ring() before you can call close_ring()"); + vtzero_assert(!m_pbf_tags.valid() && + "close_ring() has to be called before properties are added"); + vtzero_assert(m_num_points.value() == 1 && + "wrong number of points in ring"); + m_pbf_geometry.add_element(detail::command_close_path()); + m_num_points.decrement(); + } + + /** + * Add the points from the specified container as a ring to this + * feature. + * + * @tparam TContainer The container type. Must support the size() + * method, be iterable using a range for loop, and contain + * objects of type vtzero::point or something convertible to + * it. + * @param container The container to read the points from. + * + * @throws geometry_exception If there are more than 2^32-1 members in + * the container or if two consecutive points in the container + * are identical or if the last point is not the same as the + * first point. + * + * @pre You must not have any calls to add_property() before calling + * this method. + */ + template + void add_ring_from_container(const TContainer& container) { + add_ring(check_num_points(container.size())); + for (const auto& element : container) { + set_point(element); + } + } + + }; // class polygon_feature_builder + + /** + * Used for adding a feature to a layer using an existing geometry. After + * creating an object of this class you can add data to the feature in a + * specific order: + * + * * Optionally add the ID using set_id(). + * * Add the geometry using set_geometry(). + * * Optionally add any number of properties using add_property(). + * + * @code + * auto geom = ... // get geometry from a feature you are reading + * ... + * vtzero::tile_builder tb; + * vtzero::layer_builder lb{tb}; + * vtzero::geometry_feature_builder fb{lb}; + * fb.set_id(123); // optionally set ID + * fb.set_geometry(geom) // add geometry + * fb.add_property("foo", "bar"); // add property + * @endcode + */ + class geometry_feature_builder : public detail::feature_builder_base { + + public: + + /** + * Constructor + * + * @param layer The layer we want to create this feature in. + */ + explicit geometry_feature_builder(layer_builder layer) : + feature_builder_base(&layer.get_layer_impl()) { + } + + /** + * If the feature was not committed, the destructor will roll back all + * the changes. + */ + ~geometry_feature_builder() noexcept { + try { + rollback(); + } catch (...) { + // ignore exceptions + } + } + + /// Feature builders can not be copied. + geometry_feature_builder(const geometry_feature_builder&) = delete; + + /// Feature builders can not be copied. + geometry_feature_builder& operator=(const geometry_feature_builder&) = delete; + + /// Feature builders can be moved. + geometry_feature_builder(geometry_feature_builder&&) noexcept = default; + + /// Feature builders can be moved. + geometry_feature_builder& operator=(geometry_feature_builder&&) noexcept = default; + + /** + * Set the ID of this feature. + * + * You can only call this function once and it must be before calling + * set_geometry(). + * + * @param id The ID. + */ + void set_id(uint64_t id) { + vtzero_assert(m_feature_writer.valid() && + "Can not call set_id() after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid()); + set_id_impl(id); + } + + /** + * Copy the ID of an existing feature to this feature. If the + * feature doesn't have an ID, no ID is set. + * + * You can only call this function once and it must be before calling + * set_geometry(). + * + * @param feature The feature to copy the ID from. + */ + void copy_id(const feature& feature) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_id() after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid()); + if (feature.has_id()) { + set_id_impl(feature.id()); + } + } + + /** + * Set the geometry of this feature. + * + * You can only call this method once and it must be before calling the + * add_property() method. + * + * @param geometry The geometry. + */ + void set_geometry(const geometry& geometry) { + vtzero_assert(m_feature_writer.valid() && + "Can not add geometry after commit() or rollback()"); + vtzero_assert(!m_pbf_tags.valid()); + m_feature_writer.add_enum(detail::pbf_feature::type, static_cast(geometry.type())); + m_feature_writer.add_string(detail::pbf_feature::geometry, geometry.data()); + m_pbf_tags = {m_feature_writer, detail::pbf_feature::tags}; + } + + /** + * Add a property to this feature. Can only be called after the + * set_geometry method. + * + * @tparam TProp Can be type index_value_pair or property. + * @param prop The property to add. + */ + template + void add_property(TProp&& prop) { + vtzero_assert(m_feature_writer.valid() && + "Can not call add_property() after commit() or rollback()"); + add_property_impl(std::forward(prop)); + } + + /** + * Add a property to this feature. Can only be called after the + * set_geometry method. + * + * @tparam TKey Can be type index_value or data_view or anything that + * converts to it. + * @tparam TValue Can be type index_value or property_value or + * encoded_property or anything that converts to it. + * @param key The key. + * @param value The value. + */ + template + void add_property(TKey&& key, TValue&& value) { + vtzero_assert(m_feature_writer.valid() && + "Can not call add_property() after commit() or rollback()"); + add_property_impl(std::forward(key), std::forward(value)); + } + + /** + * Copy all properties of an existing feature to the one being built. + * + * @param feature The feature to copy the properties from. + */ + void copy_properties(const feature& feature) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_properties() after commit() or rollback()"); + feature.for_each_property([this](const property& prop) { + add_property_impl(prop); + return true; + }); + } + + /** + * Copy all properties of an existing feature to the one being built + * using a property_mapper. + * + * @tparam TMapper Must be the property_mapper class or something + * equivalent. + * @param feature The feature to copy the properties from. + * @param mapper Instance of the property_mapper class. + */ + template + void copy_properties(const feature& feature, TMapper& mapper) { + vtzero_assert(m_feature_writer.valid() && + "Can not call copy_properties() after commit() or rollback()"); + feature.for_each_property_indexes([this, &mapper](const index_value_pair& idxs) { + add_property_impl(mapper(idxs)); + return true; + }); + } + + /** + * Commit this feature. Call this after all the details of this + * feature have been added. If this is not called, the feature + * will be rolled back when the destructor of the feature_builder is + * called. + * + * Once a feature has been committed or rolled back, further calls + * to commit() or rollback() don't do anything. + */ + void commit() { + if (m_feature_writer.valid()) { + vtzero_assert(m_pbf_tags.valid() && + "Can not call commit before geometry was added"); + do_commit(); + } + } + + /** + * Rollback this feature. Removed all traces of this feature from + * the layer_builder. Useful when you started creating a feature + * but then find out that its geometry is invalid or something like + * it. This will also happen automatically when the feature_builder + * is destructed and commit() hasn't been called on it. + * + * Once a feature has been committed or rolled back, further calls + * to commit() or rollback() don't do anything. + */ + void rollback() { + if (m_feature_writer.valid()) { + do_rollback(); + } + } + + }; // class geometry_feature_builder + + inline void layer_builder::add_feature(const feature& feature) { + geometry_feature_builder feature_builder{*this}; + if (feature.has_id()) { + feature_builder.set_id(feature.id()); + } + feature_builder.set_geometry(feature.geometry()); + feature.for_each_property([&feature_builder](const property& p) { + feature_builder.add_property(p); + return true; + }); + feature_builder.commit(); + } + +} // namespace vtzero + +#endif // VTZERO_BUILDER_HPP diff --git a/include/vtzero/builder_impl.hpp b/include/vtzero/builder_impl.hpp new file mode 100644 index 00000000..1b348df2 --- /dev/null +++ b/include/vtzero/builder_impl.hpp @@ -0,0 +1,267 @@ +#ifndef VTZERO_BUILDER_IMPL_HPP +#define VTZERO_BUILDER_IMPL_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file builder_impl.hpp + * + * @brief Contains classes internal to the builder. + */ + +#include "encoded_property_value.hpp" +#include "property_value.hpp" +#include "types.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace vtzero { + + namespace detail { + + class layer_builder_impl { + + // If this layer is copied from an existing layer, this points + // to the data of the original layer. For newly built layers, + // this is empty. + data_view m_data_view{}; + + // Buffer containing the encoded layer metadata and features + std::string m_data; + + // Buffer containing the encoded keys table + std::string m_keys_data; + + // Buffer containing the encoded values table + std::string m_values_data; + + protozero::pbf_builder m_pbf_message_layer{}; + protozero::pbf_builder m_pbf_message_keys{}; + protozero::pbf_builder m_pbf_message_values{}; + + // The number of features in the layer + std::size_t m_num_features = 0; + + // Vector tile spec version + uint32_t m_version = 0; + + // The number of keys in the keys table + uint32_t m_num_keys = 0; + + // The number of values in the values table + uint32_t m_num_values = 0; + + // Below this value, no index will be used to find entries in the + // key/value tables. This number is based on some initial + // benchmarking but probably needs some tuning. + // See also https://github.com/mapbox/vtzero/issues/30 + static constexpr const uint32_t max_entries_flat = 20; + + using map_type = std::unordered_map; + map_type m_keys_index; + map_type m_values_index; + + static index_value find_in_table(const data_view text, const std::string& data) { + uint32_t index = 0; + protozero::pbf_message pbf_table{data}; + + while (pbf_table.next()) { + const auto v = pbf_table.get_view(); + if (v == text) { + return index_value{index}; + } + ++index; + } + + return index_value{}; + } + + // Read the key or value table and populate an index from its + // entries. This is done once the table becomes too large to do + // linear search in it. + static void populate_index(const std::string& data, map_type& map) { + uint32_t index = 0; + protozero::pbf_message pbf_table{data}; + + while (pbf_table.next()) { + map[pbf_table.get_string()] = index++; + } + } + + index_value add_value_without_dup_check(const data_view text) { + m_pbf_message_values.add_string(detail::pbf_layer::values, text); + return m_num_values++; + } + + index_value add_value(const data_view text) { + const auto index = find_in_values_table(text); + if (index.valid()) { + return index; + } + return add_value_without_dup_check(text); + } + + index_value find_in_keys_table(const data_view text) { + if (m_num_keys < max_entries_flat) { + return find_in_table(text, m_keys_data); + } + + if (m_keys_index.empty()) { + populate_index(m_keys_data, m_keys_index); + } + + auto& v = m_keys_index[std::string(text)]; + if (!v.valid()) { + v = add_key_without_dup_check(text); + } + return v; + } + + index_value find_in_values_table(const data_view text) { + if (m_num_values < max_entries_flat) { + return find_in_table(text, m_values_data); + } + + if (m_values_index.empty()) { + populate_index(m_values_data, m_values_index); + } + + auto& v = m_values_index[std::string(text)]; + if (!v.valid()) { + v = add_value_without_dup_check(text); + } + return v; + } + + public: + + // This layer should be a copy of an existing layer + explicit layer_builder_impl(const data_view data) : + m_data_view(data) { + } + + // This layer is being created from scratch + template + layer_builder_impl(TString&& name, uint32_t version, uint32_t extent) : + m_pbf_message_layer(m_data), + m_pbf_message_keys(m_keys_data), + m_pbf_message_values(m_values_data), + m_version(version) { + m_pbf_message_layer.add_uint32(detail::pbf_layer::version, version); + m_pbf_message_layer.add_string(detail::pbf_layer::name, std::forward(name)); + m_pbf_message_layer.add_uint32(detail::pbf_layer::extent, extent); + } + + ~layer_builder_impl() noexcept = default; + + layer_builder_impl(const layer_builder_impl&) = delete; + layer_builder_impl& operator=(const layer_builder_impl&) = delete; + + layer_builder_impl(layer_builder_impl&&) = default; + layer_builder_impl& operator=(layer_builder_impl&&) = default; + + uint32_t version() const noexcept { + return m_version; + } + + index_value add_key_without_dup_check(const data_view text) { + m_pbf_message_keys.add_string(detail::pbf_layer::keys, text); + return m_num_keys++; + } + + index_value add_key(const data_view text) { + const auto index = find_in_keys_table(text); + if (index.valid()) { + return index; + } + return add_key_without_dup_check(text); + } + + index_value add_value_without_dup_check(const property_value value) { + return add_value_without_dup_check(value.data()); + } + + index_value add_value_without_dup_check(const encoded_property_value& value) { + return add_value_without_dup_check(value.data()); + } + + index_value add_value(const property_value value) { + return add_value(value.data()); + } + + index_value add_value(const encoded_property_value& value) { + return add_value(value.data()); + } + + const std::string& data() const noexcept { + return m_data; + } + + const std::string& keys_data() const noexcept { + return m_keys_data; + } + + const std::string& values_data() const noexcept { + return m_values_data; + } + + protozero::pbf_builder& message() noexcept { + return m_pbf_message_layer; + } + + void increment_feature_count() noexcept { + ++m_num_features; + } + + std::size_t estimated_size() const { + if (m_data_view.data()) { + // This is a layer created as copy from an existing layer + constexpr const std::size_t estimated_overhead_for_pbf_encoding = 8; + return m_data_view.size() + estimated_overhead_for_pbf_encoding; + } + + // This is a layer created from scratch + constexpr const std::size_t estimated_overhead_for_pbf_encoding = 8; + return data().size() + + keys_data().size() + + values_data().size() + + estimated_overhead_for_pbf_encoding; + } + + template + void build(protozero::basic_pbf_builder& pbf_tile_builder) const { + if (m_data_view.data()) { + // This is a layer created as copy from an existing layer + pbf_tile_builder.add_bytes(detail::pbf_tile::layers, m_data_view); + return; + } + + // This is a layer created from scratch + if (m_num_features > 0) { + pbf_tile_builder.add_bytes_vectored(detail::pbf_tile::layers, + data(), + keys_data(), + values_data()); + } + } + + }; // class layer_builder_impl + + } // namespace detail + +} // namespace vtzero + +#endif // VTZERO_BUILDER_IMPL_HPP diff --git a/include/vtzero/encoded_property_value.hpp b/include/vtzero/encoded_property_value.hpp new file mode 100644 index 00000000..8a573f3e --- /dev/null +++ b/include/vtzero/encoded_property_value.hpp @@ -0,0 +1,244 @@ +#ifndef VTZERO_ENCODED_PROPERTY_VALUE_HPP +#define VTZERO_ENCODED_PROPERTY_VALUE_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file encoded_property_value.hpp + * + * @brief Contains the encoded_property_value class. + */ + +#include "types.hpp" + +#include + +#include +#include + +namespace vtzero { + + /** + * A property value encoded in the vector_tile internal format. Can be + * created from values of many different types and then later added to + * a layer/feature. + */ + class encoded_property_value { + + std::string m_data; + + public: + + /// Construct from vtzero::string_value_type. + explicit encoded_property_value(string_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value.value); + } + + /// Construct from const char*. + explicit encoded_property_value(const char* value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value); + } + + /// Construct from const char* and size_t. + explicit encoded_property_value(const char* value, std::size_t size) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value, size); + } + + /// Construct from std::string. + explicit encoded_property_value(const std::string& value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value); + } + + /// Construct from vtzero::data_view. + explicit encoded_property_value(const data_view& value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_string(detail::pbf_value::string_value, value); + } + + // ------------------ + + /// Construct from vtzero::float_value_type. + explicit encoded_property_value(float_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_float(detail::pbf_value::float_value, value.value); + } + + /// Construct from float. + explicit encoded_property_value(float value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_float(detail::pbf_value::float_value, value); + } + + // ------------------ + + /// Construct from vtzero::double_value_type. + explicit encoded_property_value(double_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_double(detail::pbf_value::double_value, value.value); + } + + /// Construct from double. + explicit encoded_property_value(double value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_double(detail::pbf_value::double_value, value); + } + + // ------------------ + + /// Construct from vtzero::int_value_type. + explicit encoded_property_value(int_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_int64(detail::pbf_value::int_value, value.value); + } + + /// Construct from int64_t. + explicit encoded_property_value(int64_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_int64(detail::pbf_value::int_value, value); + } + + /// Construct from int32_t. + explicit encoded_property_value(int32_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_int64(detail::pbf_value::int_value, static_cast(value)); + } + + /// Construct from int16_t. + explicit encoded_property_value(int16_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_int64(detail::pbf_value::int_value, static_cast(value)); + } + + // ------------------ + + /// Construct from vtzero::uint_value_type. + explicit encoded_property_value(uint_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_uint64(detail::pbf_value::uint_value, value.value); + } + + /// Construct from uint64_t. + explicit encoded_property_value(uint64_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_uint64(detail::pbf_value::uint_value, value); + } + + /// Construct from uint32_t. + explicit encoded_property_value(uint32_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_uint64(detail::pbf_value::uint_value, static_cast(value)); + } + + /// Construct from uint16_t. + explicit encoded_property_value(uint16_t value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_uint64(detail::pbf_value::uint_value, static_cast(value)); + } + + // ------------------ + + /// Construct from vtzero::sint_value_type. + explicit encoded_property_value(sint_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_sint64(detail::pbf_value::sint_value, value.value); + } + + // ------------------ + + /// Construct from vtzero::bool_value_type. + explicit encoded_property_value(bool_value_type value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_bool(detail::pbf_value::bool_value, value.value); + } + + /// Construct from bool. + explicit encoded_property_value(bool value) { + protozero::pbf_builder pbf_message_value{m_data}; + pbf_message_value.add_bool(detail::pbf_value::bool_value, value); + } + + // ------------------ + + /** + * Get view of the raw data stored inside. + */ + data_view data() const noexcept { + return {m_data.data(), m_data.size()}; + } + + /** + * Hash function compatible with std::hash. + */ + std::size_t hash() const noexcept { + return std::hash{}(m_data); + } + + }; // class encoded_property_value + + /// Encoded property values are equal if they contain the same data. + inline bool operator==(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() == rhs.data(); + } + + /// Encoded property values are unequal if they are not equal. + inline bool operator!=(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return !(lhs == rhs); + } + + /// Arbitrary ordering based on internal data. + inline bool operator<(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() < rhs.data(); + } + + /// Arbitrary ordering based on internal data. + inline bool operator<=(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() <= rhs.data(); + } + + /// Arbitrary ordering based on internal data. + inline bool operator>(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() > rhs.data(); + } + + /// Arbitrary ordering based on internal data. + inline bool operator>=(const encoded_property_value& lhs, const encoded_property_value& rhs) noexcept { + return lhs.data() >= rhs.data(); + } + +} // namespace vtzero + +namespace std { + + /** + * Specialization of std::hash for encoded_property_value. + */ + template <> + struct hash { + + /// key vtzero::encoded_property_value + using argument_type = vtzero::encoded_property_value; + + /// hash result: size_t + using result_type = std::size_t; + + /// calculate the hash of the argument + std::size_t operator()(const vtzero::encoded_property_value& value) const noexcept { + return value.hash(); + } + + }; // struct hash + +} // namespace std + +#endif // VTZERO_ENCODED_PROPERTY_VALUE_HPP diff --git a/include/vtzero/exception.hpp b/include/vtzero/exception.hpp new file mode 100644 index 00000000..80b72595 --- /dev/null +++ b/include/vtzero/exception.hpp @@ -0,0 +1,134 @@ +#ifndef VTZERO_EXCEPTION_HPP +#define VTZERO_EXCEPTION_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file exception.hpp + * + * @brief Contains the exceptions used in the vtzero library. + */ + +#include +#include +#include + +namespace vtzero { + + /** + * Base class for all exceptions directly thrown by the vtzero library. + */ + class exception : public std::runtime_error { + + public: + + /// Constructor + explicit exception(const char* message) : + std::runtime_error(message) { + } + + /// Constructor + explicit exception(const std::string& message) : + std::runtime_error(message) { + } + + }; // class exception + + /** + * This exception is thrown when vector tile encoding isn't valid according + * to the vector tile specification. + */ + class format_exception : public exception { + + public: + + /// Constructor + explicit format_exception(const char* message) : + exception(message) { + } + + /// Constructor + explicit format_exception(const std::string& message) : + exception(message) { + } + + }; // class format_exception + + /** + * This exception is thrown when a geometry encoding isn't valid according + * to the vector tile specification. + */ + class geometry_exception : public format_exception { + + public: + + /// Constructor + explicit geometry_exception(const char* message) : + format_exception(message) { + } + + /// Constructor + explicit geometry_exception(const std::string& message) : + format_exception(message) { + } + + }; // class geometry_exception + + /** + * This exception is thrown when a property value is accessed using the + * wrong type. + */ + class type_exception : public exception { + + public: + + /// Constructor + explicit type_exception() : + exception("wrong property value type") { + } + + }; // class type_exception + + /** + * This exception is thrown when an unknown version number is found in the + * layer. + */ + class version_exception : public exception { + + public: + + /// Constructor + explicit version_exception(const uint32_t version) : + exception(std::string{"unknown vector tile version: "} + + std::to_string(version)) { + } + + }; // version_exception + + /** + * This exception is thrown when an index into the key or value table + * in a layer is out of range. This can only happen if the tile data is + * invalid. + */ + class out_of_range_exception : public exception { + + public: + + /// Constructor + explicit out_of_range_exception(const uint32_t index) : + exception(std::string{"index out of range: "} + + std::to_string(index)) { + } + + }; // out_of_range_exception + +} // namespace vtzero + +#endif // VTZERO_EXCEPTION_HPP diff --git a/include/vtzero/feature.hpp b/include/vtzero/feature.hpp new file mode 100644 index 00000000..745d49a1 --- /dev/null +++ b/include/vtzero/feature.hpp @@ -0,0 +1,315 @@ +#ifndef VTZERO_FEATURE_HPP +#define VTZERO_FEATURE_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file feature.hpp + * + * @brief Contains the feature class. + */ + +#include "exception.hpp" +#include "property.hpp" +#include "property_value.hpp" +#include "types.hpp" + +#include + +#include +#include +#include +#include + +namespace vtzero { + + class layer; + + /** + * A feature according to spec 4.2. + * + * Note that a feature will internally contain a pointer to the layer it + * came from. The layer has to stay valid as long as the feature is used. + */ + class feature { + + using uint32_it_range = protozero::iterator_range; + + const layer* m_layer = nullptr; + uint64_t m_id = 0; // defaults to 0, see https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L32 + uint32_it_range m_properties{}; + protozero::pbf_reader::const_uint32_iterator m_property_iterator{}; + std::size_t m_num_properties = 0; + data_view m_geometry{}; + GeomType m_geometry_type = GeomType::UNKNOWN; // defaults to UNKNOWN, see https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L41 + bool m_has_id = false; + + public: + + /** + * Construct an invalid feature object. + */ + feature() = default; + + /** + * Construct a feature object. + * + * @throws format_exception if the layer data is ill-formed. + */ + feature(const layer* layer, const data_view data) : + m_layer(layer) { + vtzero_assert(layer); + vtzero_assert(data.data()); + + protozero::pbf_message reader{data}; + + while (reader.next()) { + switch (reader.tag_and_type()) { + case protozero::tag_and_type(detail::pbf_feature::id, protozero::pbf_wire_type::varint): + m_id = reader.get_uint64(); + m_has_id = true; + break; + case protozero::tag_and_type(detail::pbf_feature::tags, protozero::pbf_wire_type::length_delimited): + if (m_properties.begin() != protozero::pbf_reader::const_uint32_iterator{}) { + throw format_exception{"Feature has more than one tags field"}; + } + m_properties = reader.get_packed_uint32(); + m_property_iterator = m_properties.begin(); + break; + case protozero::tag_and_type(detail::pbf_feature::type, protozero::pbf_wire_type::varint): { + const auto type = reader.get_enum(); + // spec 4.3.4 "Geometry Types" + if (type < 0 || type > 3) { + throw format_exception{"Unknown geometry type (spec 4.3.4)"}; + } + m_geometry_type = static_cast(type); + } + break; + case protozero::tag_and_type(detail::pbf_feature::geometry, protozero::pbf_wire_type::length_delimited): + if (!m_geometry.empty()) { + throw format_exception{"Feature has more than one geometry field"}; + } + m_geometry = reader.get_view(); + break; + default: + reader.skip(); // ignore unknown fields + } + } + + // spec 4.2 "A feature MUST contain a geometry field." + if (m_geometry.empty()) { + throw format_exception{"Missing geometry field in feature (spec 4.2)"}; + } + + const auto size = m_properties.size(); + if (size % 2 != 0) { + throw format_exception{"unpaired property key/value indexes (spec 4.4)"}; + } + m_num_properties = size / 2; + } + + /** + * Is this a valid feature? Valid features are those not created from + * the default constructor. + * + * Complexity: Constant. + */ + bool valid() const noexcept { + return m_geometry.data() != nullptr; + } + + /** + * Is this a valid feature? Valid features are those not created from + * the default constructor. + * + * Complexity: Constant. + */ + explicit operator bool() const noexcept { + return valid(); + } + + /** + * The ID of this feature. According to the spec IDs should be unique + * in a layer if they are set (spec 4.2). + * + * Complexity: Constant. + * + * Always returns 0 for invalid features. + */ + uint64_t id() const noexcept { + return m_id; + } + + /** + * Does this feature have an ID? + * + * Complexity: Constant. + * + * Always returns false for invalid features. + */ + bool has_id() const noexcept { + return m_has_id; + } + + /** + * The geometry type of this feature. + * + * Complexity: Constant. + * + * Always returns GeomType::UNKNOWN for invalid features. + */ + GeomType geometry_type() const noexcept { + return m_geometry_type; + } + + /** + * Get the geometry of this feature. + * + * Complexity: Constant. + * + * @pre @code valid() @endcode + */ + vtzero::geometry geometry() const noexcept { + vtzero_assert_in_noexcept_function(valid()); + return {m_geometry, m_geometry_type}; + } + + /** + * Returns true if this feature doesn't have any properties. + * + * Complexity: Constant. + * + * Always returns true for invalid features. + */ + bool empty() const noexcept { + return m_num_properties == 0; + } + + /** + * Returns the number of properties in this feature. + * + * Complexity: Constant. + * + * Always returns 0 for invalid features. + */ + std::size_t num_properties() const noexcept { + return m_num_properties; + } + + /** + * Get the next property in this feature. + * + * Complexity: Constant. + * + * @returns The next property or the invalid property if there are no + * more properties. + * @throws format_exception if the feature data is ill-formed. + * @throws any protozero exception if the protobuf encoding is invalid. + * @pre @code valid() @endcode + */ + property next_property(); + + /** + * Get the indexes into the key/value table for the next property in + * this feature. + * + * Complexity: Constant. + * + * @returns The next index_value_pair or an invalid index_value_pair + * if there are no more properties. + * @throws format_exception if the feature data is ill-formed. + * @throws out_of_range_exception if the key or value index is not + * within the range of indexes in the layer key/value table. + * @throws any protozero exception if the protobuf encoding is invalid. + * @pre @code valid() @endcode + */ + index_value_pair next_property_indexes(); + + /** + * Reset the property iterator. The next time next_property() or + * next_property_indexes() is called, it will begin from the first + * property again. + * + * Complexity: Constant. + * + * @pre @code valid() @endcode + */ + void reset_property() noexcept { + vtzero_assert_in_noexcept_function(valid()); + m_property_iterator = m_properties.begin(); + } + + /** + * Call a function for each property of this feature. + * + * @tparam TFunc The type of the function. It must take a single + * argument of type property&& and return a bool. If the + * function returns false, the iteration will be stopped. + * @param func The function to call. + * @returns true if the iteration was completed and false otherwise. + * @pre @code valid() @endcode + */ + template + bool for_each_property(TFunc&& func) const; + + /** + * Call a function for each key/value index of this feature. + * + * @tparam TFunc The type of the function. It must take a single + * argument of type index_value_pair&& and return a bool. + * If the function returns false, the iteration will be stopped. + * @param func The function to call. + * @returns true if the iteration was completed and false otherwise. + * @pre @code valid() @endcode + */ + template + bool for_each_property_indexes(TFunc&& func) const; + + }; // class feature + + /** + * Create some kind of mapping from property keys to property values. + * + * This can be used to read all properties into a std::map or similar + * object. + * + * @tparam TMap Map type (std::map, std::unordered_map, ...) Must support + * the emplace() method. + * @tparam TKey Key type, usually the key of the map type. The data_view + * of the property key is converted to this type before + * adding it to the map. + * @tparam TValue Value type, usally the value of the map type. The + * property_value is converted to this type before + * adding it to the map. + * @tparam TMapping A struct derived from property_value_mapping with the + * mapping for vtzero property value types to TValue-constructing + * types. (See convert_property_value() for details.) + * @param feature The feature to get the properties from. + * @returns An object of type TMap with all the properties. + * @pre @code feature.valid() @endcode + */ + template + TMap create_properties_map(const vtzero::feature& feature) { + TMap map; + + feature.for_each_property([&map](const property& p) { + map.emplace(TKey(p.key()), convert_property_value(p.value())); + return true; + }); + + return map; + } + +} // namespace vtzero + +#endif // VTZERO_FEATURE_HPP diff --git a/include/vtzero/feature_builder_impl.hpp b/include/vtzero/feature_builder_impl.hpp new file mode 100644 index 00000000..30674186 --- /dev/null +++ b/include/vtzero/feature_builder_impl.hpp @@ -0,0 +1,126 @@ +#ifndef VTZERO_FEATURE_BUILDER_IMPL_HPP +#define VTZERO_FEATURE_BUILDER_IMPL_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file feature_builder_impl.hpp + * + * @brief Contains classes internal to the builder. + */ + +#include "builder_impl.hpp" +#include "encoded_property_value.hpp" +#include "geometry.hpp" +#include "property.hpp" +#include "property_value.hpp" + +#include + +namespace vtzero { + + namespace detail { + + class feature_builder_base { + + layer_builder_impl* m_layer; + + void add_key_internal(index_value idx) { + vtzero_assert(idx.valid()); + m_pbf_tags.add_element(idx.value()); + } + + template + void add_key_internal(T&& key) { + add_key_internal(m_layer->add_key(data_view{std::forward(key)})); + } + + void add_value_internal(index_value idx) { + vtzero_assert(idx.valid()); + m_pbf_tags.add_element(idx.value()); + } + + void add_value_internal(property_value value) { + add_value_internal(m_layer->add_value(value)); + } + + template + void add_value_internal(T&& value) { + encoded_property_value v{std::forward(value)}; + add_value_internal(m_layer->add_value(v)); + } + + protected: + + protozero::pbf_builder m_feature_writer; + protozero::packed_field_uint32 m_pbf_tags; + + explicit feature_builder_base(layer_builder_impl* layer) : + m_layer(layer), + m_feature_writer(layer->message(), detail::pbf_layer::features) { + } + + ~feature_builder_base() noexcept = default; + + feature_builder_base(const feature_builder_base&) = delete; // NOLINT(hicpp-use-equals-delete, modernize-use-equals-delete) + + feature_builder_base& operator=(const feature_builder_base&) = delete; // NOLINT(hicpp-use-equals-delete, modernize-use-equals-delete) + // The check wants these functions to be public... + + feature_builder_base(feature_builder_base&&) noexcept = default; + + feature_builder_base& operator=(feature_builder_base&&) noexcept = default; + + uint32_t version() const noexcept { + return m_layer->version(); + } + + void set_id_impl(uint64_t id) { + m_feature_writer.add_uint64(detail::pbf_feature::id, id); + } + + void add_property_impl(const property& property) { + add_key_internal(property.key()); + add_value_internal(property.value()); + } + + void add_property_impl(const index_value_pair idxs) { + add_key_internal(idxs.key()); + add_value_internal(idxs.value()); + } + + template + void add_property_impl(TKey&& key, TValue&& value) { + add_key_internal(std::forward(key)); + add_value_internal(std::forward(value)); + } + + void do_commit() { + if (m_pbf_tags.valid()) { + m_pbf_tags.commit(); + } + m_feature_writer.commit(); + m_layer->increment_feature_count(); + } + + void do_rollback() { + if (m_pbf_tags.valid()) { + m_pbf_tags.rollback(); + } + m_feature_writer.rollback(); + } + + }; // class feature_builder_base + + } // namespace detail + +} // namespace vtzero + +#endif // VTZERO_FEATURE_BUILDER_IMPL_HPP diff --git a/include/vtzero/geometry.hpp b/include/vtzero/geometry.hpp new file mode 100644 index 00000000..42af6236 --- /dev/null +++ b/include/vtzero/geometry.hpp @@ -0,0 +1,444 @@ +#ifndef VTZERO_GEOMETRY_HPP +#define VTZERO_GEOMETRY_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file geometry.hpp + * + * @brief Contains classes and functions related to geometry handling. + */ + +#include "exception.hpp" +#include "types.hpp" + +#include + +#include +#include +#include + +namespace vtzero { + + /// A simple point class + struct point { + + /// X coordinate + int32_t x = 0; + + /// Y coordinate + int32_t y = 0; + + /// Default construct to 0 coordinates + constexpr point() noexcept = default; + + /// Constructor + constexpr point(int32_t x_, int32_t y_) noexcept : + x(x_), + y(y_) { + } + + }; // struct point + + /** + * Type of a polygon ring. This can either be "outer", "inner", or + * "invalid". Invalid is used when the area of the ring is 0. + */ + enum class ring_type { + outer = 0, + inner = 1, + invalid = 2 + }; // enum class ring_type + + /** + * Helper function to create a point from any type that has members x + * and y. + * + * If your point type doesn't have members x any y, you can overload this + * function for your type and it will be used by vtzero. + */ + template + point create_vtzero_point(const TPoint& p) noexcept { + return {p.x, p.y}; + } + + /// Points are equal if their coordinates are + inline constexpr bool operator==(const point a, const point b) noexcept { + return a.x == b.x && a.y == b.y; + } + + /// Points are not equal if their coordinates aren't + inline constexpr bool operator!=(const point a, const point b) noexcept { + return !(a == b); + } + + namespace detail { + + /// The command id type as specified in the vector tile spec + enum class CommandId : uint32_t { + MOVE_TO = 1, + LINE_TO = 2, + CLOSE_PATH = 7 + }; + + inline constexpr uint32_t command_integer(CommandId id, const uint32_t count) noexcept { + return (static_cast(id) & 0x7U) | (count << 3U); + } + + inline constexpr uint32_t command_move_to(const uint32_t count) noexcept { + return command_integer(CommandId::MOVE_TO, count); + } + + inline constexpr uint32_t command_line_to(const uint32_t count) noexcept { + return command_integer(CommandId::LINE_TO, count); + } + + inline constexpr uint32_t command_close_path() noexcept { + return command_integer(CommandId::CLOSE_PATH, 1); + } + + inline constexpr uint32_t get_command_id(const uint32_t command_integer) noexcept { + return command_integer & 0x7U; + } + + inline constexpr uint32_t get_command_count(const uint32_t command_integer) noexcept { + return command_integer >> 3U; + } + + // The maximum value for the command count according to the spec. + inline constexpr uint32_t max_command_count() noexcept { + return get_command_count(std::numeric_limits::max()); + } + + inline constexpr int64_t det(const point a, const point b) noexcept { + return static_cast(a.x) * static_cast(b.y) - + static_cast(b.x) * static_cast(a.y); + } + + template + struct get_result { + + using type = void; + + template + void operator()(TGeomHandler&& /*geom_handler*/) const noexcept { + } + + }; + + template + struct get_result().result()), void>::value>::type> { + + using type = decltype(std::declval().result()); + + template + type operator()(TGeomHandler&& geom_handler) { + return std::forward(geom_handler).result(); + } + + }; + + /** + * Decode a geometry as specified in spec 4.3 from a sequence of 32 bit + * unsigned integers. This templated base class can be instantiated + * with a different iterator type for testing than for normal use. + */ + template + class geometry_decoder { + + public: + + using iterator_type = TIterator; + + private: + + iterator_type m_it; + iterator_type m_end; + + point m_cursor{0, 0}; + + // maximum value for m_count before we throw an exception + uint32_t m_max_count; + + /** + * The current count value is set from the CommandInteger and + * then counted down with each next_point() call. So it must be + * greater than 0 when next_point() is called and 0 when + * next_command() is called. + */ + uint32_t m_count = 0; + + public: + + geometry_decoder(iterator_type begin, iterator_type end, std::size_t max) : + m_it(begin), + m_end(end), + m_max_count(static_cast(max)) { + vtzero_assert(max <= detail::max_command_count()); + } + + uint32_t count() const noexcept { + return m_count; + } + + bool done() const noexcept { + return m_it == m_end; + } + + bool next_command(const CommandId expected_command_id) { + vtzero_assert(m_count == 0); + + if (m_it == m_end) { + return false; + } + + const auto command_id = get_command_id(*m_it); + if (command_id != static_cast(expected_command_id)) { + throw geometry_exception{std::string{"expected command "} + + std::to_string(static_cast(expected_command_id)) + + " but got " + + std::to_string(command_id)}; + } + + if (expected_command_id == CommandId::CLOSE_PATH) { + // spec 4.3.3.3 "A ClosePath command MUST have a command count of 1" + if (get_command_count(*m_it) != 1) { + throw geometry_exception{"ClosePath command count is not 1"}; + } + } else { + m_count = get_command_count(*m_it); + if (m_count > m_max_count) { + throw geometry_exception{"count too large"}; + } + } + + ++m_it; + + return true; + } + + point next_point() { + vtzero_assert(m_count > 0); + + if (m_it == m_end || std::next(m_it) == m_end) { + throw geometry_exception{"too few points in geometry"}; + } + + // spec 4.3.2 "A ParameterInteger is zigzag encoded" + int64_t x = protozero::decode_zigzag32(*m_it++); + int64_t y = protozero::decode_zigzag32(*m_it++); + + // x and y are int64_t so this addition can never overflow + x += m_cursor.x; + y += m_cursor.y; + + // The cast is okay, because a valid vector tile can never + // contain values that would overflow here and we don't care + // what happens to invalid tiles here. + m_cursor.x = static_cast(x); + m_cursor.y = static_cast(y); + + --m_count; + + return m_cursor; + } + + template + typename detail::get_result::type decode_point(TGeomHandler&& geom_handler) { + // spec 4.3.4.2 "MUST consist of a single MoveTo command" + if (!next_command(CommandId::MOVE_TO)) { + throw geometry_exception{"expected MoveTo command (spec 4.3.4.2)"}; + } + + // spec 4.3.4.2 "command count greater than 0" + if (count() == 0) { + throw geometry_exception{"MoveTo command count is zero (spec 4.3.4.2)"}; + } + + geom_handler.points_begin(count()); + while (count() > 0) { + geom_handler.points_point(next_point()); + } + + // spec 4.3.4.2 "MUST consist of of a single ... command" + if (!done()) { + throw geometry_exception{"additional data after end of geometry (spec 4.3.4.2)"}; + } + + geom_handler.points_end(); + + return detail::get_result{}(std::forward(geom_handler)); + } + + template + typename detail::get_result::type decode_linestring(TGeomHandler&& geom_handler) { + // spec 4.3.4.3 "1. A MoveTo command" + while (next_command(CommandId::MOVE_TO)) { + // spec 4.3.4.3 "with a command count of 1" + if (count() != 1) { + throw geometry_exception{"MoveTo command count is not 1 (spec 4.3.4.3)"}; + } + + const auto first_point = next_point(); + + // spec 4.3.4.3 "2. A LineTo command" + if (!next_command(CommandId::LINE_TO)) { + throw geometry_exception{"expected LineTo command (spec 4.3.4.3)"}; + } + + // spec 4.3.4.3 "with a command count greater than 0" + if (count() == 0) { + throw geometry_exception{"LineTo command count is zero (spec 4.3.4.3)"}; + } + + geom_handler.linestring_begin(count() + 1); + + geom_handler.linestring_point(first_point); + while (count() > 0) { + geom_handler.linestring_point(next_point()); + } + + geom_handler.linestring_end(); + } + + return detail::get_result{}(std::forward(geom_handler)); + } + + template + typename detail::get_result::type decode_polygon(TGeomHandler&& geom_handler) { + // spec 4.3.4.4 "1. A MoveTo command" + while (next_command(CommandId::MOVE_TO)) { + // spec 4.3.4.4 "with a command count of 1" + if (count() != 1) { + throw geometry_exception{"MoveTo command count is not 1 (spec 4.3.4.4)"}; + } + + int64_t sum = 0; + const point start_point = next_point(); + point last_point = start_point; + + // spec 4.3.4.4 "2. A LineTo command" + if (!next_command(CommandId::LINE_TO)) { + throw geometry_exception{"expected LineTo command (spec 4.3.4.4)"}; + } + + geom_handler.ring_begin(count() + 2); + + geom_handler.ring_point(start_point); + + while (count() > 0) { + const point p = next_point(); + sum += detail::det(last_point, p); + last_point = p; + geom_handler.ring_point(p); + } + + // spec 4.3.4.4 "3. A ClosePath command" + if (!next_command(CommandId::CLOSE_PATH)) { + throw geometry_exception{"expected ClosePath command (spec 4.3.4.4)"}; + } + + sum += detail::det(last_point, start_point); + + geom_handler.ring_point(start_point); + + geom_handler.ring_end(sum > 0 ? ring_type::outer : + sum < 0 ? ring_type::inner : ring_type::invalid); + } + + return detail::get_result{}(std::forward(geom_handler)); + } + + }; // class geometry_decoder + + } // namespace detail + + /** + * Decode a point geometry. + * + * @tparam TGeomHandler Handler class. See tutorial for details. + * @param geometry The geometry as returned by feature.geometry(). + * @param geom_handler An object of TGeomHandler. + * @throws geometry_error If there is a problem with the geometry. + * @pre Geometry must be a point geometry. + */ + template + typename detail::get_result::type decode_point_geometry(const geometry& geometry, TGeomHandler&& geom_handler) { + vtzero_assert(geometry.type() == GeomType::POINT); + detail::geometry_decoder decoder{geometry.begin(), geometry.end(), geometry.data().size() / 2}; + return decoder.decode_point(std::forward(geom_handler)); + } + + /** + * Decode a linestring geometry. + * + * @tparam TGeomHandler Handler class. See tutorial for details. + * @param geometry The geometry as returned by feature.geometry(). + * @param geom_handler An object of TGeomHandler. + * @returns whatever geom_handler.result() returns if that function exists, + * void otherwise + * @throws geometry_error If there is a problem with the geometry. + * @pre Geometry must be a linestring geometry. + */ + template + typename detail::get_result::type decode_linestring_geometry(const geometry& geometry, TGeomHandler&& geom_handler) { + vtzero_assert(geometry.type() == GeomType::LINESTRING); + detail::geometry_decoder decoder{geometry.begin(), geometry.end(), geometry.data().size() / 2}; + return decoder.decode_linestring(std::forward(geom_handler)); + } + + /** + * Decode a polygon geometry. + * + * @tparam TGeomHandler Handler class. See tutorial for details. + * @param geometry The geometry as returned by feature.geometry(). + * @param geom_handler An object of TGeomHandler. + * @returns whatever geom_handler.result() returns if that function exists, + * void otherwise + * @throws geometry_error If there is a problem with the geometry. + * @pre Geometry must be a polygon geometry. + */ + template + typename detail::get_result::type decode_polygon_geometry(const geometry& geometry, TGeomHandler&& geom_handler) { + vtzero_assert(geometry.type() == GeomType::POLYGON); + detail::geometry_decoder decoder{geometry.begin(), geometry.end(), geometry.data().size() / 2}; + return decoder.decode_polygon(std::forward(geom_handler)); + } + + /** + * Decode a geometry. + * + * @tparam TGeomHandler Handler class. See tutorial for details. + * @param geometry The geometry as returned by feature.geometry(). + * @param geom_handler An object of TGeomHandler. + * @returns whatever geom_handler.result() returns if that function exists, + * void otherwise + * @throws geometry_error If the geometry has type UNKNOWN of if there is + * a problem with the geometry. + */ + template + typename detail::get_result::type decode_geometry(const geometry& geometry, TGeomHandler&& geom_handler) { + detail::geometry_decoder decoder{geometry.begin(), geometry.end(), geometry.data().size() / 2}; + switch (geometry.type()) { + case GeomType::POINT: + return decoder.decode_point(std::forward(geom_handler)); + case GeomType::LINESTRING: + return decoder.decode_linestring(std::forward(geom_handler)); + case GeomType::POLYGON: + return decoder.decode_polygon(std::forward(geom_handler)); + default: + break; + } + throw geometry_exception{"unknown geometry type"}; + } + +} // namespace vtzero + +#endif // VTZERO_GEOMETRY_HPP diff --git a/include/vtzero/index.hpp b/include/vtzero/index.hpp new file mode 100644 index 00000000..75f4df73 --- /dev/null +++ b/include/vtzero/index.hpp @@ -0,0 +1,264 @@ +#ifndef VTZERO_INDEX_HPP +#define VTZERO_INDEX_HPP + +/***************************************************************************** + +vtzero - Tiny and fast vector tile decoder and encoder in C++. + +This file is from https://github.com/mapbox/vtzero where you can find more +documentation. + +*****************************************************************************/ + +/** + * @file index.hpp + * + * @brief Contains classes for indexing the key/value tables inside layers. + */ + +#include "builder.hpp" +#include "types.hpp" + +#include +#include +#include + +namespace vtzero { + + /** + * Used to store the mapping between property keys and the index value + * in the table stored in a layer. + * + * @tparam TMap The map class to use (std::map, std::unordered_map or + * something compatible). + */ + template