React-native-mapbox-gl: [v6] segfaults when subscribing to offline pack download, then trying to render a MapView

Created on 15 Nov 2017  Â·  4Comments  Â·  Source: nitaliano/react-native-mapbox-gl

I'll try to create up a minimal repro tonight, but here are the snippets of code that seem to be causing problems for me. I'm using a Redux Thunk to trigger the initial map download, e.g.

// redux/modules/downloads.js

export function beginDownload(destinationId) {
  return async dispatch => {

    // ...

    const progressListener = (offlinePack, status) => dispatch(onMapDownloadProgress(offlinePack, status))
    const errorListener = (offlinePack, err) => console.warn(offlinePack, err)
    await MapboxGL.offlineManager.createPack({
      name: packName,
      styleURL: MapboxGL.StyleURL.Street,
      minZoom: 10,
      maxZoom: 20,
      bounds: [[neLng, neLat], [swLng, swLat]],
    }, progressListener, errorListener) // one subscriber here
  }
}

export function onMapDownloadProgress(offlinePack, status) {
  return async dispatch => {
    const {name, percentage, state} = status
    const idStr = name.replace('pack_', '')
    const id = parseInt(idStr, 10)

    if (state === MapboxGL.OfflinePackDownloadState.Complete) {
      dispatch({type: DOWNLOAD_COMPLETE, value: id})
      return
    }

    dispatch({
      type: MAP_DOWNLOAD_PROGRESS,
      value: { destinationId: id, percentage},
    })
  }
}

then on a "download status" component subscribe to download updates again

// note #onMapDownloadProgress from above is bound using redux's #mapDispatchToProps

class DownloadableScreen extends React.Component {
  constructor (props) {
    super(props)

    this.state = {
      packs: [],
    }
  }

  async componentWillMount() {
    const {onMapDownloadProgress} = this.props
    const errorListener = (offlinePack, err) => console.warn(offlinePack, err)

    const offlinePacks = await MapboxGL.offlineManager.getPacks()
    const packs = offlinePacks.map(pack => pack._metadata.name)
    console.log('packs====', packs)
    this.setState({packs})

    for (let packName of packs) {
      MapboxGL.offlineManager.subscribe(packName, onMapDownloadProgress, errorListener)
    }
  }

  componentWillUnmount () {
    let uniq = a => [...new Set(a)]

    const packs = uniq([
      ...this.state.packs, // Provided by #getPacks (previously stored on phone)
      ...this.props.packs, // Provided by newly triggered download
    ])

    for (let packName of packs) {
      MapboxGL.offlineManager.unsubscribe(packName)
    }
  }

// ...
}

at first I thought this code was working great, but then later on, with no code changes, I noticed that my app was crashing a lot, especially when I switched to a tab that rendered the MapView (but also just sitting there with it open) and when it crashes I see this in the logs:

Nov 15 08:57:34 ss-MacBook-Pro appyapp[60082] <Notice>: —— log end ——
Nov 15 08:57:35 ss-MacBook-Pro SpringBoard[59272] <Error>: [KeyboardArbiter] HW kbd: Failed to set (null) as keyboard focus
Nov 15 08:57:35 ss-MacBook-Pro com.apple.CoreSimulator.SimDevice.5050E7B8-BD8C-4ED0-BCB2-A52F60D04200.launchd_sim[59255] (UIKitApplication:com.xyz.appyapp[0x221e][59277][60082]) <Notice>: Service exited due to Segmentation fault: 11
Nov 15 08:57:35 ss-MacBook-Pro backboardd[59273] <Error>: [Common] Unable to get short BSD proc info for 60082: No such process
Nov 15 08:57:35 ss-MacBook-Pro backboardd[59273] <Error>: [Common] Unable to get proc info for 60082: Undefined error: 0
Nov 15 08:57:36 ss-MacBook-Pro assertiond[59277] <Warning>: Deleted job with label: UIKitApplication:com.xyz.appyapp[0x221e][59277]
Nov 15 08:57:40 ss-MacBook-Pro locationd[59275] <Notice>: Location icon should now be in state 'Inactive'

a couple times before I also saw malloc: * error for object xxx: pointer being freed was not allocated * set a breakpoint in malloc_error_break to debug but when I tried to set the breakpoint it didn't seem to have any effect.

if I comment out the componentWillMount/componentWillUnmount code, the segfault disappears. I wonder if the double-subscribing is causing the issue, and if so, how I might adapt my code to prevent this. The fact that my code worked well for a time period, before bugging out irretrievably makes this smell like a memory leak.

Let me know if there's anything else I can do to help debug - sorry for being new at this but I'm not even sure how to generate a more verbose crash log. I can definitely code up a quick minimal repro later today, if that's helpful.

Most helpful comment

So I finally figured out the root cause of all these seg faults. Basically, if you make a call to any of MapboxGL.offlineManager's methods BEFORE the onDidFinishLoadingMap event fires and the map's style is loaded, you will get seg faults.

In my use case, a user might attempt to download a map even before the screen where the MapView is rendered is ever tapped! Therefore, I had to add the following to the first screen of my app:

        <MapArea
          style={{height: 0, width: 0, flex: 0,/* HACK for https://github.com/mapbox/react-native-mapbox-gl/issues/766#issuecomment-346449740 */}}
          onDidFinishLoadingMap={this.props.mapStylesLoaded}
        />

where mapStylesLoaded is just a redux action that sets loadedStyles: false in my store to true. Then, in my downloadStuff action, I can explicitly check that loadedStyles is true before proceeding. Not the most elegant hack in the world (I'm open to suggestions!) but I'm really glad I was able to figure out this race condition that kept causing my app to crash.

Thanks @nitaliano for the CreateOfflineRegion.js example that helped me figure out where to look.

All 4 comments

@smoll I'll start looking into this, I think what you provided is good enough for me to get going. I do have one question is this only happening to you on iOS?

@nitaliano I haven't been able to spin up Android due to an unrelated issue blocking me from concurrent development.

I did want to share one piece of info though:

I discovered that when I unsubscribe inside my reducer, once the download is fully complete, the crashes stopped. To be more explicit, I did this:

// redux/modules/downloads.js

function onMapDownloadProgress(offlinePack, status, dispatch) {
  // ...
  if (state === MapboxGL.OfflinePackDownloadState.Complete) {
    dispatch({type: DOWNLOAD_COMPLETE, value: {destinationId: id, unsub: true}}) // <- here
    db.run(`UPDATE destinations SET map_pack = '${name}' where id = '${id}';`)
    return
  }
  // ...
}

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
  // ...
    case DOWNLOAD_COMPLETE: {
      const {destinationId, unsub} = action.value
      const packName = `pack_${destinationId}`
      if (unsub) MapboxGL.offlineManager.unsubscribe(packName) // <- unsubscribes the 2nd listener
      return {
        ...state,
        statuses: {
          ...state.statuses,
          ...{
            [destinationId]: {
              status: COMPLETE,
            },
          }
        }
      }
    }
  }
}

so it could be my original error was just that I was never correctly unsubscribing the listener created by MapboxGL.offlineManager.createPack

Before I made this code change, I instrumented my app with Sentry, and it was able to capture the following error, which I think is related to my originally reported issue: https://sentry.io/share/issue/f3e771618b4d465ea72298e815840e73/

Sorry for the noise on my end, but I could definitely see other users making a similar error, especially when using Mapbox with Redux, so this thread is good implicit documentation 😅

I'm going to close this out, I plan on making some redux and mobx examples so this will be good to put into the examples

So I finally figured out the root cause of all these seg faults. Basically, if you make a call to any of MapboxGL.offlineManager's methods BEFORE the onDidFinishLoadingMap event fires and the map's style is loaded, you will get seg faults.

In my use case, a user might attempt to download a map even before the screen where the MapView is rendered is ever tapped! Therefore, I had to add the following to the first screen of my app:

        <MapArea
          style={{height: 0, width: 0, flex: 0,/* HACK for https://github.com/mapbox/react-native-mapbox-gl/issues/766#issuecomment-346449740 */}}
          onDidFinishLoadingMap={this.props.mapStylesLoaded}
        />

where mapStylesLoaded is just a redux action that sets loadedStyles: false in my store to true. Then, in my downloadStuff action, I can explicitly check that loadedStyles is true before proceeding. Not the most elegant hack in the world (I'm open to suggestions!) but I'm really glad I was able to figure out this race condition that kept causing my app to crash.

Thanks @nitaliano for the CreateOfflineRegion.js example that helped me figure out where to look.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

yaduc picture yaduc  Â·  3Comments

lernerbot picture lernerbot  Â·  3Comments

EugenePisotsky picture EugenePisotsky  Â·  4Comments

alexisohayon picture alexisohayon  Â·  4Comments

Craytor picture Craytor  Â·  3Comments