I'm working on a pothole map web app in Google Apps Script. The map gets its data from a Google spreadsheet (from manual surveying), which is then transformed via hits to the Geolocation and Google Maps Roads APIs. There are 101 markers on the map (one marker per row of data, which represents either a pothole or a cluster of potholes). This map, for some odd reasons, takes about 20 seconds to load initially. Unacceptable!
This app makes use of the following APIs:
- jQuery
- Google Maps Roads
- Google Geolocation
- async.js
- Mustache.js
I profiled my code , and from what it seems, most of the CPU time is from methods that don't appear to be business ones. I analyzed the code profile two ways:
Self time
There were at least three heavy function calls in this respect, although nothing that seems to be too bad.
Total time
There was no business logic right at the top of this list, but plenty right below it. I present thus:
I then identify the lines of code where these bottlenecks appear:
addPotholeMarker()
/* Adds marker to map.
* Parameters :
* • potholeData : a PotholeData (or PotholeDataFromCoords) object
* • snappedToRoad: boolean
* • callback : the function to call next (optional)
* Returns :
* • the marker that was added to the map, or null if arguments invalid, or callback() if it was provided
*/
function addPotholeMarker(potholeData, snappedToRoad, callback) {
// make sure that callback is either falsy or a function
if ((callback) && (typeof callback !== 'function')) throw new TypeError('callback specified but not a function. Thrown from addPotholeMarker()');
// make sure potholeState is either falsy or contains iconURL string
if ((!potholeData.potholeState) || ((potholeData.potholeState) && (potholeData.potholeState.iconURL === undefined))) throw new Error('invalid potholeData');
// let's make sure to snap this to road if it isn't already...
var coords = new GPSCoordinates(potholeData.lat, potholeData.lng);
if (!snappedToRoad)
{
var potholeMarker = 'a garbage return value';
getRoadCoordinates(coords).done(function(response) {
var coords = response.snappedPoints[0].location;
potholeData.lat = coords.latitude;
potholeData.lng = coords.longitude;
potholeData.isSnappedToRoad(true);
return (potholeMarker = addPotholeMarker(potholeData, true, callback));
/* potholeMarker = addPotholeMarker(potholeData, true);
return potholeMarker;*/
});
if (callback) return callback(null);
return;
//return potholeMarker;
}
var marker = new google.maps.Marker({
position: coords,
title: coords.toString(),
map: map,
opacity: 0.5,
icon: ((potholeData.potholeState.iconURL !== undefined) ? potholeData.potholeState.iconURL : PURPLE_MARKER)
});
// make marker have effect when mouseout,mouseover
marker.addListener('mouseover', function(mouseEvent) {
marker.setOpacity(1.0);
});
marker.addListener('mouseout', function(mouseEvent) {
marker.setOpacity(0.5);
});
var infoWindow = createInfoWindow(potholeData);
// save infoWindow for later reference
infoWindows[statesMap.get(getPotholeStateFor(potholeData))].push(infoWindow);
// on click of marker, show infoWindow
marker.addListener('click', function(mouseEvent) {
infoWindow.open(map, marker);
});
// add this to potholeMarkers
potholeMarkers[statesMap.get(getPotholeStateFor(potholeData))].push(marker);
if (callback) return callback(null, marker);
return marker;
}
snapPotholeCoordsToRoad()
/* snaps potholes stored in potholeCoordinates to road
* Parameters:
* • potholeCollection : (Object<Array<PotholeData> >) the Collection of PotholeData to use
* • callback : the function to call next
*/
// TODO: refactor the body of this so as to use potholeCollection (preferrably instead of potholeCoordinates)
function snapPotholeCoordsToRoad(potholeCollection, callback)
{
var DEBUG = false;
// guarantee that callback is function
if ((callback) && (typeof(callback) !== 'function')) throw new TypeError('callback is something, but not a function. Thrown from snapPotholeCoordsToRoad().');
// for each element of potholeCollection
if (DEBUG) console.log('potholeCollection === ' + JSON.stringify(potholeCollection, null, '\t'));
var keys = [];
for (var key in potholeCollection)
{
if (typeof potholeCollection[key] !== 'function') keys.push(key);
}
for (var key in potholeCollection)
{
(function itr(k, m) {
if (typeof potholeCollection[k] === 'function') return;
if (m === potholeCollection[k].length) return;
if (DEBUG) console.log('potholeCollection.' + k + '[' + m + '] == ' + JSON.stringify(potholeCollection[k][m], null, '\t'));
// if element (PotholeData) not snapped to road
if (!potholeCollection[k][m].isSnappedToRoad())
{
// get road coordinates for element
getRoadCoordinates(potholeCollection[k][m])
// replace element's coordinates with those road coordinates
.done(function(newCoords) {
potholeCollection[k][m].setCoordinates(newCoords.snappedPoints[0].location);
//debugger;
potholeCollection[k][m].isSnappedToRoad(true);
if (DEBUG) console.log('potholeCollection.' + k + '[' + m + '] == ' + JSON.stringify(potholeCollection[k][m], null, '\t'));
if ((k === keys[keys.length - 1]) && (m === potholeCollection[k].length - 1) && (callback)) return callback(null, null, potholeCollection);
itr(k, m+1);
})
}
else
{
if ((k === keys[keys.length - 1]) && (m === potholeCollection[k].length - 1) && (callback)) return callback(null, null, potholeCollection);
itr(k, m+1);
}
})(key, 0);
}
}
addPotholeMarkers()
/* put all potholes on the map
* Parameters:
* • callback : the function to call next
* • unsnappedPotholes: (<Object<Array<PotholeData> > ) collection of potholes with coordinates that have not been snapped to road
* • snappedPotholes : (<Object<Array<PotholeData> > ) collection of potholes with coordinates that are snapped to road
*/
function addPotholeMarkers(unsnappedPotholes, snappedPotholes, callback)
{
var DEBUG = false;
// guarantee that callback is function
if ((callback) && (typeof(callback) !== 'function')) throw new TypeError('callback is something, but not a function. Thrown from addPotholeMarkers().');
// add all the markers for them to the map
async.waterfall([
function(cb) {
async.eachOf(unsnappedPotholes, function(value, key) {
// TODO: refactor this
async.eachOf(value, function (v, k) {
if (DEBUG) console.log('dealing with unsnappedPotholes');
//console.assert(v.potholeState.toString() != '[object Object]', 'toString() override missing');
//v = revivePotholeState(v); // unnecessary, because addPotholeMarker() invokes getPotholeStateFor() to force the potholeState to be in the statesMap
addPotholeMarker(v, false);
})
})
cb(null);
}, function(cb) {
async.eachOf(snappedPotholes, function(value, key) {
async.eachSeries(value, function(pothole, fn) {
if (DEBUG) console.log('pothole == ' + JSON.stringify(pothole, null, '\t'));
addPotholeMarker(pothole,
true,
//pothole.isSnappedToRoad(),
fn);
})
})
cb(null);
}], function(err, results) {
console.log('trying to center map');
adjustMap();
console.log('Map recentered');
if (callback) {
callback(err);
}
});
}
The driver function
google.script.run.withSuccessHandler(function(data) {
async.waterfall([//fetchServerPotholeData, // for some reason, this function is not receiving data
function(callback) {
fetchServerPotholeData(data, callback);
},
fetchCoords,
snapPotholeCoordsToRoad,
addPotholeMarkers
], function(err, result) {
if (!err) {
console.log('Everything successfully done. Enjoy your map!');
}
else
{
console.error(err);
}
}
)
}).withFailureHandler(console.log).getPotholeData();
I'm trying to fix these bottlenecks but am not sure how.
UPDATE
I'm adding a link to the GitHub project itself that I created the other day, since this question's been crickets since I asked it.
Here is the HTML code to the app (maybe the script is wasting a lot of time loading the necessary <script>
tags):
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script
type="text/javascript"
src="https://maps.googleapis.com/maps/api/js?libraries=places,geocoding,geometry&key=AIzaSyCcjfkPZ0EfqZnyJrOZ3cuqdAWTFlXmFxM&callback=initMap"
async defer></script>
<script
src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/0.7.0/mustache.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/async.min.js"></script>
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
<!--<script src="https://google-developers.appspot.com/_static/js/jquery-bundle.js"></script>-->
<script src="//code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<?!= include('mathFunctions'); ?>
<?!=// include('Pothole'); ?>
<?!= include('JavaScript'); ?>
<?!= include('stylesheet'); ?>
</head>
<body>
<div id="toggleHidden" class="floatingMenu">—</div>
<div id="legend" class="floatingMenu">
<span class="center row title">Legend</span>
<!-- the different icons and states of potholes -->
<div>
</div>
</div>
<div id="map">
</div>
</body>
</html>
addPotholeMarker
, which is invoked at most 101 * 2 == 202 times. Google Maps API documentation advises against dropping a bunch of markers all at once: developers.google.com/maps/documentation/javascript/… , at least fro adding animation (which I am not trying to do). Perhaps I could remedy that by writing a wrapperfunction(potholeData, snappedToRoad, someTime, callback) { setTimeout(addPotholeMarker(potholeData, snappedToRoad, callback), someTime) }
? \$\endgroup\$