Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

body recursive fetch_all without Enum.reverse #205

Merged
merged 1 commit into from Jun 1, 2022
Merged

body recursive fetch_all without Enum.reverse #205

merged 1 commit into from Jun 1, 2022

Conversation

ruslandoga
Copy link
Contributor

closes #204

{:done, rows} ->
case accum do
[] -> {:ok, rows}
accum -> {:ok, Enum.reverse(rows ++ accum)}
Copy link
Contributor Author

@ruslandoga ruslandoga Jun 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rows should've been reversed here before concatenating, I think

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. They should have been reversed, before appending.

@ruslandoga
Copy link
Contributor Author

ruslandoga commented Jun 1, 2022

And since the reason for changing fetch_all was performance, here's a naive benchmark showing that the performance is unchanged.

bench.exs

Mix.install([:benchee, :exqlite])
alias Exqlite.Sqlite3

defmodule Setup do
  def create_users(conn, count) do
    :ok = Sqlite3.execute(conn, "create table users (id integer primary key, name text)")
    {:ok, stmt} = Sqlite3.prepare(conn, "insert into users values (?, ?)")
    Sqlite3.execute(conn, "begin")

    Enum.each(1..count, fn i ->
      :ok = Sqlite3.bind(conn, stmt, [i, "User-" <> to_string(i)])
      :done = Sqlite3.step(conn, stmt)
    end)

    Sqlite3.execute(conn, "commit")
  end
end

defmodule Tail do
  def fetch_all(conn, statement) do
    fetch_all(conn, statement, 50, [])
  end

  defp fetch_all(conn, statement, chunk_size, accum) do
    case Sqlite3.multi_step(conn, statement, chunk_size) do
      {:done, rows} ->
        case accum do
          [] -> {:ok, rows}
          accum -> {:ok, Enum.reverse(rows ++ accum)}
        end

      {:rows, rows} ->
        fetch_all(conn, statement, chunk_size, Enum.reverse(rows) ++ accum)

      {:error, reason} ->
        {:error, reason}

      :busy ->
        {:error, "Database busy"}
    end
  end
end

defmodule Body do
  def fetch_all(conn, statement) do
    {:ok, try_fetch_all(conn, statement, 50)}
  catch
    :throw, {:error, _reason} = error -> error
  end

  defp try_fetch_all(conn, statement, chunk_size) do
    case Sqlite3.multi_step(conn, statement, chunk_size) do
      {:done, rows} -> rows
      {:rows, rows} -> rows ++ try_fetch_all(conn, statement, chunk_size)
      {:error, _reason} = error -> throw(error)
      :busy -> throw({:error, "Database busy"})
    end
  end
end

{:ok, conn} = Sqlite3.open(":memory:")
:ok = Setup.create_users(conn, 10000)
{:ok, stmt} = Sqlite3.prepare(conn, "select * from users")

Benchee.run(
  %{
    "body-recursive" => fn -> Body.fetch_all(conn, stmt) end,
    "tail-recursive" => fn -> Tail.fetch_all(conn, stmt) end
  },
  memory_time: 2
)
> elixir bench.exs

Operating System: macOS
CPU Information: Apple M1
Number of Available Cores: 8
Available memory: 8 GB
Elixir 1.13.4
Erlang 25.0

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 2 s
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 18 s

Benchmarking body-recursive ...
Benchmarking tail-recursive ...

Name                     ips        average  deviation         median         99th %
body-recursive        134.14        7.45 ms    ±14.22%        7.45 ms        9.39 ms
tail-recursive        131.50        7.60 ms    ±16.37%        7.63 ms        9.66 ms

Comparison:
body-recursive        134.14
tail-recursive        131.50 - 1.02x slower +0.150 ms

Memory usage statistics:

Name              Memory usage
body-recursive         1.49 MB
tail-recursive         1.70 MB - 1.14x memory usage +0.21 MB

**All measurements for memory usage were the same**

@warmwaffles warmwaffles merged commit 09bcd7f into elixir-sqlite:main Jun 1, 2022
@warmwaffles
Copy link
Member

Fix released as v0.11.2 thanks @ruslandoga

@ruslandoga ruslandoga deleted the body-recursive-fetch-all-without-enum-reverse branch June 1, 2022 14:34
@SebastianSzturo
Copy link
Contributor

Thanks for fixing this properly! Looks like the initial bottle neck was not the body recursion 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

fetch_all breaks row order
3 participants