/// <summary>
        /// This approach uses a single large NativeArray of samples filled in by each job instead of adding temporary DynamicBuffers to the entities.
        /// This appears to be the best balance between speed and simplicity.
        ///
        /// Test results:
        /// Total: ~40ms
        /// Setup: ~0ms
        /// Combine: 2.27ms
        /// </summary>
        private List <List <IntPoint> > PolygonsFromRoadOutlineEntityForEachSeparateBuffer(float extensionDistance, JobHandle jobHandle)
        {
            Profiler.BeginSample("PolygonsFromRoadOutlineEntityForEachSeparateBuffer");
            Profiler.BeginSample("PolygonsFromRoadOutlineEntityForEachSeparateBuffer_Setup");
            var polygons = new List <List <IntPoint> >();

            var sampleStartIndexMap = new NativeHashMap <Entity, RoadSampleSpan>(300, Allocator.TempJob);
            var sampleCountTotal    = 0;

            Entities.ForEach((int entityInQueryIndex, Entity entity, ref EcsRoad road) =>
            {
                var sampleCount             = GeometrySampling.ComputeSampleCountForRoadOutline(road, .5f);
                sampleStartIndexMap[entity] = new RoadSampleSpan
                {
                    startIndex = sampleCountTotal,
                    count      = sampleCount
                };
                sampleCountTotal += sampleCount;
            }).Run();

            var samples = new NativeArray <PointSampleGlobal>(sampleCountTotal, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);

            var job = Entities.ForEach((Entity entity,
                                        ref EcsRoad road,
                                        in DynamicBuffer <Geometry> ecsGeometries,
                                        in DynamicBuffer <EcsLane> ecsLanes,
                                        in DynamicBuffer <EcsLaneSection> ecsLaneSections,
                                        in DynamicBuffer <LaneOffset> laneOffsets,
                                        in DynamicBuffer <LaneWidthRecord> laneWidthRecords) => //(Entity entity, ref EcsRoad road) =>
            {
                var ecsRoadData = new EcsRoadData()
                {
                    ecsRoad          = road,
                    ecsGeometries    = ecsGeometries,
                    ecsLaneSections  = ecsLaneSections,
                    ecsLanes         = ecsLanes,
                    laneOffsets      = laneOffsets,
                    laneWidthRecords = laneWidthRecords
                };;
                var roadSampleSpan = sampleStartIndexMap[entity];
                GeometrySampling.BuildSamplesFromRoadOutlineWithExtensionDistance(
                    ecsRoadData, 0.5f, extensionDistance, new NativeSlice <PointSampleGlobal>(samples, roadSampleSpan.startIndex, roadSampleSpan.count));
            }).WithReadOnly(sampleStartIndexMap).WithoutBurst().Schedule(jobHandle);
        /// <summary>
        /// This approach extends the idea in Job.WithCode() by manually generating batches of work with one job per batch for better load balancing.
        /// Setup is tedious, but is slightly faster (~1-2ms) than Entities.ForEach with a separate buffer. We should probably avoid this pattern due to the large overhead and brittle code.
        ///
        /// Test results:
        /// Total: 36ms
        /// Setup: 1.12ms
        /// Combine: 2.12ms
        /// </summary>
        private List <List <IntPoint> > PolygonsFromRoadOutlineIJobParallelFor(float extensionDistance, JobHandle jobHandle)
        {
            Profiler.BeginSample("PolygonsFromRoadOutlineIJobParallelFor");
            Profiler.BeginSample("PolygonsFromRoadOutlineIJobParallelFor_Setup");
            var polygons = new List <List <IntPoint> >();

            var outlineJobParams = new NativeList <OutlineJobParams>(300, Allocator.TempJob);
            var sampleCountTotal = 0;

            Entities.ForEach((int entityInQueryIndex, Entity entity, ref EcsRoad road) =>
            {
                var sampleCount = GeometrySampling.ComputeSampleCountForRoadOutline(road, .5f);
                outlineJobParams.Add(new OutlineJobParams
                {
                    entity     = entity,
                    startIndex = sampleCountTotal,
                    count      = sampleCount
                });
                sampleCountTotal += sampleCount;
            }).Run();

            var samples = new NativeArray <PointSampleGlobal>(sampleCountTotal, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);


            var ecsRoadGetter         = GetComponentDataFromEntity <EcsRoad>(true);
            var geometryGetter        = GetBufferFromEntity <Geometry>(true);
            var laneSectionGetter     = GetBufferFromEntity <EcsLaneSection>(true);
            var laneGetter            = GetBufferFromEntity <EcsLane>(true);
            var laneOffsetGetter      = GetBufferFromEntity <LaneOffset>(true);
            var laneWidthRecordGetter = GetBufferFromEntity <LaneWidthRecord>(true);

            var batchSize              = samples.Length / 64;
            var jobBatches             = new NativeList <JobHandle>(Allocator.TempJob);
            int currentBatchSize       = 0;
            int currentBatchStartIndex = 0;

            for (int i = 0; i < outlineJobParams.Length; i++)
            {
                var currentJobParams = outlineJobParams[i];
                currentBatchSize += currentJobParams.count;
                if (currentBatchSize >= batchSize || i == outlineJobParams.Length - 1)
                {
                    var job = new BuildSamplesJob()
                    {
                        extensionDistance = extensionDistance,
                        outlineJobBatch   = new OutlineJobBatch
                        {
                            startIndex = currentBatchStartIndex,
                            count      = i - currentBatchStartIndex + 1
                        },
                        outlineJobParams      = outlineJobParams,
                        ecsRoadGetter         = ecsRoadGetter,
                        geometryGetter        = geometryGetter,
                        laneSectionGetter     = laneSectionGetter,
                        laneGetter            = laneGetter,
                        laneOffsetGetter      = laneOffsetGetter,
                        laneWidthRecordGetter = laneWidthRecordGetter,
                        samples = samples
                    }.Schedule(jobHandle);
                    jobBatches.Add(job);

                    currentBatchSize       = 0;
                    currentBatchStartIndex = i + 1;
                }
            }

            Profiler.EndSample();
            Profiler.BeginSample("PolygonsFromRoadOutlineIJobParallelFor_CompleteJob");
            JobHandle.CombineDependencies(jobBatches).Complete();

            Profiler.EndSample();
            Profiler.BeginSample("PolygonsFromRoadOutlineIJobParallelFor_Combine");

            foreach (var outlineJobParam in outlineJobParams)
            {
                var sampleSlice = new NativeSlice <PointSampleGlobal>(samples, outlineJobParam.startIndex, outlineJobParam.count);
                var path        = new List <IntPoint>(sampleSlice.Length);
                foreach (var point in sampleSlice)
                {
                    path.Add(new IntPoint(
                                 point.pose.pos.x * PlacementUtility.UpScaleFactor,
                                 point.pose.pos.z * PlacementUtility.UpScaleFactor));
                }

                if (!Clipper.Orientation(path))
                {
                    path.Reverse();
                }

                polygons.Add(path);
            }

            outlineJobParams.Dispose();
            jobBatches.Dispose();
            samples.Dispose();
            Profiler.EndSample();
            Profiler.EndSample();

            return(polygons);
        }