Ensuring all tiles are loaded in Open Layers 3 XYZ source

This is my approach. It uses an undocumented API, but it works in non-debug openlayers 4.2.0.

//"Dirty" tiles can be in one of two states: Either they are being downloaded,
//or the map is holding off downloading their replacement, and they are "wanted."
//We can tell when the map is ready when there are no tiles in either of these
//states, and rendering is done.

var numInFlightTiles = 0;
map.getLayers().forEach(function (layer) {
    var source = layer.getSource();
    if (source instanceof ol.source.TileImage) {
        source.on('tileloadstart', function () {++numInFlightTiles})
        source.on('tileloadend', function () {--numInFlightTiles})
    }
})

map.on('postrender', function (evt) {
    if (!evt.frameState)
        return;

    var numHeldTiles = 0;
    var wanted = evt.frameState.wantedTiles;
    for (var layer in wanted)
        if (wanted.hasOwnProperty(layer))
            numHeldTiles += Object.keys(wanted[layer]).length;

    var ready = numInFlightTiles === 0 && numHeldTiles === 0;
    if (map.get('ready') !== ready)
        map.set('ready', ready);
});

map.set('ready', false);

function whenMapIsReady(callback) {
    if (map.get('ready'))
        callback();
    else
        map.once('change:ready', whenMapIsReady.bind(null, callback));
}

Loading events

You are correct in assuming that each tileloadstart event on the source should be followed by a tileloadend or tileloaderror for the corresponding tile. That can be used, as in the linked official example, to keep track of the number of loading tiles.

When the sum of emitted tileloadend and tileloaderror events equal the number of tileloadstart events, no loading is in progress. If this is not the case, you should try to make a reproducible example, as it would probably be a bug in the library.

It is however important to understand what these events mean. The tileloadend event does not mean that the tile is visible on the map, it means that the tile has finished loading and is usable for rendering. The actual rendering of the tile will be done after the event handler is invoked. So any tile loading logic requiring information about when all tiles are loaded and rendered (such when taking screenshots/creating prints) will have to wait until the next postrender event.

You mention 5-10 seconds between a tileloadend and the tile actually appearing on the map, which is too long for it to be rendering related (unless you do some really freaky rendering callbacks).

ol-debug.js vs ol.js

Like many JS libraries, OpenLayers code is optimized and minimized in the build process to create smaller and more efficient builds. Any type or function that is not part of the API will be minified or removed. Only the methods available in ol.js, and documented on openlayers.org, should be used as any minified methods may change each build.

ol-debug.js is a non-optimized version of the library, intended for use when debugging or exploring.